赞
踩
目录
快速排序顾名思义就是使其可以快速的从小到大排序,按排序后的数组输出。题型:
给定你一个长度为 n的整数数列。
请你使用快速排序对这个数列按照从小到大进行排序。
并将排好序的数列按顺序输出。
输入格式
输入共两行,第一行包含整数 n。
第二行包含 n 个整数(所有整数均在 1∼1091∼109 范围内),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤1000001≤n≤100000
输入样例:
5 3 2 5 4 1输出样例:
1 2 3 4 5
步骤:
划清界限:x=[left+right]/2。使左边的值都小于这个值,右边的值都大于这个值。
初始化两个指针,一个指向最左边,一个指向最右边。
如果两个指针中其中都不满足要求,则交换两个指针所指向的值。
之后再将分完的两边再进行一次排序。
那就开始写吧:
- #include <iostream>
-
- using namespace std;
-
- const int N = 1e6+10;
-
- int n=0;
- int q[N]={0};
-
- void quick_sort(int *q,int Left,int Right)
- {
- if(Left>=Right) return;
-
- int x=q[(Left+Right)/2],i=Left-1,j=Right+1;
-
- while(i<j)
- {
- do i++; while(q[i]<x);
- do j--; while(q[j]>x);
-
- if(i<j) swap(q[i],q[j]);
- }
-
- quick_sort(q,Left,j);
- quick_sort(q,j+1,Right);
- }
-
- int main()
- {
- scanf("%d",&n);
- for(int i=0;i<n;i++) scanf("%d",&q[i]);
-
- quick_sort(q,0,n-1);
-
- for(int i=0;i<n;i++) printf("%d ",q[i]);
-
- return 0;
- }
拓展:求第k个数
给定一个长度为n的整数数列,以及一个整数k,请用快速选择算法求出数列从小到大排序后的第k个数。
输入格式
第一行包含两个整数 n 和 k。
第二行包含 n个整数(所有整数均在 1∼1091∼109 范围内),表示整数数列。
输出格式
输出一个整数,表示数列的第 k 小数。
数据范围
1≤n≤100000, 1≤k≤n
输入样例:
5 3 2 4 1 5 3输出样例:
3
步骤:
先使用快速排序进行排序。
判断想要取的数在哪个区间
快排那个区间,不断缩小范围
最后返回快排到最后只剩下一个的数组。
代码:
- #include <cstring>
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int N = 100010;
-
- int n, k;
- int q[N];
-
- int quick_select(int l, int r)
- {
- if (l == r) return q[l];
-
- int i = l - 1, j = r + 1, x = q[l + r >> 1];
- while (i < j)
- {
- do i ++ ; while (q[i] < x);
- do j -- ; while (q[j] > x);
- if (i < j) swap(q[i], q[j]);
- }
-
- if (k <= j) return quick_select(l, j);
- return quick_select(j + 1, r);
- }
-
- int main()
- {
- scanf("%d%d", &n, &k);
-
- for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
-
- k -- ;
- cout << quick_select(0, n - 1) << endl;
-
- return 0;
- }
归并排序与快速排序有什么区别?快速排序是在一段数组空间中进行比大小排序,而归并排序是将数据分为两段,先将两段数据排好序,之后再用两个指针去遍历这两段排好序的数组。将两个数组中指向的值,较为小的值存放进另外一个数组中,之后再将指针指向下一个数。
题目:
给定你一个长度为 n 的整数数列。
请你使用归并排序对这个数列按照从小到大进行排序。
并将排好序的数列按顺序输出。
输入格式
输入共两行,第一行包含整数 n。
第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5 3 1 2 4 5输出样例:
1 2 3 4 5
步骤:
将输入的数组分为两个部分。
将两个部分进行快排
快排后再使用两个指针进行操作,将较小的那个指针所对应的值存放进另外一个数组中。
若还有一个指针没有遍历完,那么将这个指针后面的值继续存放至数组中。
将另外一个数组的值存放进原数组中。
代码:
- #include <iostream>
-
- using namespace std;
-
- const int N = 1e6+10;
-
- int n=0;
- int a[N]={0},tmp[N]={0};
-
- void merger(int Left,int Right,int *a)
- {
- if(Left>=Right) return;
-
- int mid=(Left+Right)>>1;
-
- merger(Left,mid,a);
- merger(mid+1,Right,a);
-
- int k=0,l=Left,r=mid+1;
-
- while(l<=mid&&r<=Right)
- {
- if(a[l]<=a[r]) tmp[k++]=a[l++];
- else tmp[k++]=a[r++];
- }
-
- while(l<=mid) tmp[k++]=a[l++];
- while(r<=Right) tmp[k++]=a[r++];
-
- for(int i=Left,j=0;i<=Right;i++,j++)
- {
- a[i]=tmp[j];
- }
- }
-
- int main()
- {
- scanf("%d",&n);
-
- for(int i=0;i<n;i++) scanf("%d",&a[i]);
-
- merger(0,n-1,a);
-
- for(int i=0;i<n;i++) printf("%d ",a[i]);
-
- return 0;
- }
拓展:求逆序对的数量。
其实这道题还有一种方法,就是使用哈希,但是到后面再用哈希解决吧,现在先用归并排序试试看。
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i个和第 j个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
输入格式
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000 数列中的元素的取值范围 [1,10^9]。
输入样例:
6 2 3 4 5 6 1输出样例:
5
步骤:
先把整个区间分为两部分。[L,R] => [L,MID],[MID+1,R]
递归排序
归并,将左右两个有序序列合并成一个有序序列。
那么假设在归并的同时又可以读出逆序对的数量,那么可以有三种情况。
第一钟情况是左半边内部的逆序对数量:merge(L,mid)
第二种情况是右半边内部的逆序对数量:merger(mid+1,R)
第三种情况是逆序对一部分存放在左半边,另外一部分存放在右半边,那么对于已经排序好的左右半边,如果存在一个q左>q右,那么就说明从q左往右就是一直是q右的逆序对,只需要求出其长度count = mid-Left+1即可。
- #include <iostream>
-
- using namespace std;
-
- typedef long long LL;
-
- const int N = 100010;
-
- int n=0;
- int q[N]={0},tmp[N]={0};
-
- LL merger(int Left,int Right,int* q)
- {
- if(Left>=Right) return 0;
-
- int mid = Left+Right>>1;
- LL res=merger(Left,mid,q)+merger(mid+1,Right,q);
-
- //开始归并
- int k=0,i=Left,j=mid+1;
- while(i<=mid && j<= Right)
- {
- if(q[i]<=q[j]) tmp[k++]=q[i++];
- else
- {
- tmp[k++]=q[j++];
- res += mid-i+1;
- }
- }
-
- //扫尾补齐
- while(i<=mid) tmp[k++]=q[i++];
- while(j<=Right) tmp[k++]=q[j++];
-
- //返回
- for(int i=Left,j=0;i<=Right;i++,j++) q[i]=tmp[j];
-
- return res;
- }
-
- int main()
- {
- cin >> n;
- for(int i=0;i<n;i++) cin>>q[i];
-
- cout<<merger(0,n-1,q)<<endl;
-
- return 0;
- }
二分法可应用于:求数值在数组中所在的位置、高精度算数、数的范围。。。
题目:数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 00 开始计数)。
如果数组中不存在该元素,则返回
-1 -1
。输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼100001∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回
-1 -1
。数据范围
1≤n≤10000 1≤q≤1000 1≤k≤10000
输入样例:
6 3 1 2 2 3 3 4 3 4 5输出样例:
3 4 5 5 -1 -1
这种类型的题目,由于其数组已经排列好了,那么就可以划分一条界限,根据那个界限可以去确定所需要的元素在哪个区间,进而可以确定在哪个位置,不断地缩小该区间,知道找到该值的位置为止。这种题存在着数组越界的风险,虽然看着简单,但是数组越界的时候真的让人抓不着头脑。这道题是一个包含了重复元素的有序序列,要求输出某元素的起始位置以及终止位置,如果查找不到,那就返回-1,需要写两个二分,一个需要找到>=x的第一个数,另一个需要找到<=x的最后一个数。
步骤:
确定左右边界
算出中间边界,具有两种情况(Left+Right)/2或者是(Left+Right+1)/2这两种情况。
根据值的范围,不断地缩小左右区间。
那么现在来确定这两种情况需要怎么实现:
情况一:在右半段寻找左边界(即寻找符合性质的第一个点)
当存在有array[mid]<x时,令Left=mid+1,mid及其左边的位置被排除了,那么出现的解的位置就是mid+1及其后面的位置了。区间划分为[Left,mid]和[mid+1,Right]。当查找结束时,Left和Right是会相遇的,Left所在的元素x则一定是x出现的最小位置,因为Left左边的元素必然都小于x。
- int Left=0,Right=n-1;
- while(Left<Right)
- {
- int mid=Left+Right>>1;
- if(array[mid]<x) Left=mid+1;
- else Right=mid;
- }
- return Left;
情况二:在左半段寻找右边界(即寻找不符合性质的最后一个点)
将区间分为[Left,mid-1]和[mid,Right](如果mid点符合性值,那么划分的右边界必定从mid-1开始)
- int Left=0,Right=n-1;
- while(Left<Right)
- {
- int mid=(Left+Right+1)>>2;
- if(array[mid]>x) Right=mid-1;
- else Left=mid;
- }
- return Left;
其中的mid=(Left+Right+1)/2这里,+1是为了除法向下取整,当在Right=Left+1时更新Left=mid时会出现Left=mid=Left的死循环。+1就实现了向上取整,可以解决这个隐患。
那么上面就已经得出了二分的两个模板了,那么下面开始写题目的程序吧:
- #include <iostream>
-
- using namespace std;
-
- const int N = 1e6+10;
-
- int n,m;
- int q[N];
-
- int main()
- {
- scanf("%d %d",&n,&m);
- for(int i=0;i<n;i++) scanf("%d",&q[i]);
-
- while(m--)
- {
- int x;
- scanf("%d",&x);
-
- int Left=0,Right=n-1;
-
- while(Left<Right)
- {
- int mid=(Left+Right)>>1;
- if(q[mid]>=x) Right=mid;
- else Left=mid+1;
- }
-
- if(q[Left]!=x) cout<<"-1 -1"<<endl;
- else
- {
- cout<<Left<<' ';
-
- int Left=0,Right=n-1;
- while(Left<Right)
- {
- int mid=(Left+Right+1)>>1;
- if(q[mid]<=x) Left=mid;
- else Right=mid-1;
- }
-
- cout<<Left<<endl;
- }
- }
-
- return 0;
- }
拓展:数的三次方根
给定一个浮点数 n,求它的三次方根。
输入格式
共一行,包含一个浮点数 n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留 66 位小数。
数据范围
−10000≤n≤10000
输入样例:
1000.00输出样例:
10.000000
关于浮点数的二分:
确定边界,这里的n的区间为[-10000,10000]
进行二分,对于一个数开三次方根,如果它的数是存在于边界的左边,那么就可以将区间锁定为左边界,如果它的数是存在于边界的右边,那么就将区间锁定为有边界。
保留六位小数的这个细节:这里使用了1e-8,如果使用1e10-7,那么会因为四舍五入第八位到第七位,已经产生误差,输出六位的时候误差会积累。如果使用-8,那么四舍五入第九位到低八位,取前六位的时候,第七位就会被truncate掉,但是第七位是准确值。所以正常要预留小数点多少位的时候,要留出两位四舍五入的空间。
代码如下:
- #include <iostream>
-
- using namespace std;
-
- int main()
- {
- double x;
- cin>>x;
-
- double l=-10000,r=10000;
- while(r-1>1e-8)
- {
- double mid = (l+r)/2;
- if(mid*mid*mid>=x) r=mid;
- else l=mid;
- }
- printf("%lf\n",l);
-
- return 0;
- }
高精度算法的情景:
两个大整数相加
两个大整数相减
一个大整数乘以一个小整数
求一个大整数除以一个小整数的商和余数
关于大整数的存储:
申请一个数组,从数组的第一个元素开始存放数字的最低位,之后依次向下存放。为什么要将低位放在数组的开头,高位放在数组的后面,因为进位是需要操作高位的,使用push_back这个函数比较方便。
一、高精度加法
对于题目如下:
给定两个正整数(不含前导 00),计算它们的和。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的和。
数据范围
1≤整数长度≤100000
输入样例:
12 23输出样例:
35
步骤:
申请一个大的容器vector
思考我们平时按位加法的步骤,按位加,若有进位,那么下一位+1,现在的这一位取余数。
假设进位为t,那么就存在这这一位的计算为A+B+t。
对于加法:
- #include <iostream>
- #include <vector>
-
- using namespace std;
-
- vector<int> add(vector<int>&A,vector<int>&B)
- {
- vector<int> c;
-
- int t=0;
- for(int i=0;i<A.size()||i<B.size();i++)
- {
- //在有效的位数中相加
- if(i<A.size()) t+=A[i];
- if(i<B.size()) t+=B[i];
-
- //保留
- c.push_back(t%10);
- //进位
- t/=10;
- }
-
- //若到了最后还有进位,那就在最高位+1
- if(t) c.push_back(1);
- return c;
- }
-
- int main()
- {
- string a,b;
- vector<int> A,B;
-
- cin>>a>>b;
-
- //实现最高位存放在数组的第一位
- for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
- for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
-
- auto c=add(A,B);
-
- for(int i=c.size()-1;i>=0;i--) printf("%d",c[i]);
-
- return 0;
- }
那么对于减法:
给定两个正整数(不含前导 00),计算它们的差,计算结果可能为负数。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的差。
数据范围
1≤整数长度≤105
输入样例:
32 11输出样例:
21
申请一个容器vector
判断两个数的大小,取决哪个数为被减数。
进行减法运算:模拟我们正常算减法时的情景:当我们减法时,如果需要借位,那么下一位的取值应该时应该有A-B-t。
当有借位的位时,需要补上+10,如果不需要的话,+10后最后余10的出来的数也为0,所以不影响,直接就把两种情况包容了。
- #include <iostream>
- #include <vector>
-
- using namespace std;
-
- bool cmp(vector<int>& A, vector<int> &B)
- {
- if(A.size() != B.size()) return A.size() > B.size(); //直接ruturn 了就不用else
-
- for(int i = A.size(); i >= 0; i--)
- if(A[i] != B[i])
- return A[i] > B[i];
-
- return true;
- }
-
- vector <int> sub(vector<int>& A, vector<int> &B)
- {
- vector<int> C;
- int t = 0;
- for(int i = 0; i < A.size(); i++)
- {
- t = A[i] - t;
- if(i < B.size()) t -= B[i];
- C.push_back((t + 10) % 10 ); //对于借位的处理,包容了需要借位以及不需要借位的两种情况。
- if(t < 0) t = 1;
- else t = 0;
-
- }
-
- while(C.size() > 1 && C.back() == 0) C.pop_back(); //去掉前导0
-
- return C;
- }
-
- int main()
- {
- string a ,b;
- vector<int> A, B;
-
- cin >> a >> b ;
-
- for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
- for(int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');
-
- if (cmp(A,B))
- {
- auto C = sub(A, B);
- for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
- return 0;
- }
- else
- {
- auto C = sub(B, A);
- printf("-");
- for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
- return 0;
- }
-
-
- }
那么对于乘法:
给定两个非负整数(不含前导 00) AA 和 BB,请你计算 A×BA×B 的值。
输入格式
共两行,第一行包含整数 AA,第二行包含整数 BB。
输出格式
共一行,包含 A×BA×B 的值。
数据范围
1≤A的长度≤100000 0≤B≤10000
输入样例:
2 3输出样例:
6
步骤:
当进行乘法时,相应的位上的数为A*B%10。
下一位时,需要算出进位的值t为A*B/10。那么这一位就是(A * B+ T)%10。
当我们进行乘法时,其实跟我们运算顺序是不一样的,我们正常乘法是会将多位数的B按位进行相乘。但是在程序中不是这样的,这样子太麻烦了,正常是把它看为一个整体进行相乘。
重复
代码如下:
- #include <iostream>
- #include <vector>
-
- using namespace std;
-
- vector<int> mul(vector<int> &A,int b)
- {
- vector<int> c;
- int t=0;
- for(int i=0;i<A.size()||t;i++)
- {
- if(i<A.size()) t+=A[i]*b;
- c.push_back(t%10);
- t/=10;
- }
-
- //除去前导0
- while(c.size()>1&&c.back()==0) c.pop_back();
- return c;
- }
-
- int main()
- {
- string a;
- int b;
-
- cin >> a >> b;
-
- vector<int> A;
- for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
-
- auto c=mul(A,b);
-
- for(int i=c.size()-1;i>=0;i--) printf("%d",c[i]);
-
- return 0;
- }
那么对于除法:
给定两个非负整数(不含前导 00) A,BA,B,请你计算 A/BA/B 的商和余数。
输入格式
共两行,第一行包含整数 AA,第二行包含整数 BB。
输出格式
共两行,第一行输出所求的商,第二行输出所求余数。
数据范围
1≤A的长度≤100000 1≤B≤10000 B一定不为 0
输入样例:
7 2输出样例:
3 1
步骤:
对于正常的除法运算,我们都是先取第一个数,看看这个数是否能除以我们要除的除数,之后将求得的商写在上面,余数保留给下一位用。
求得的余数还需要乘上10再加上下一位数,就可以得到下一位要运算的数值。
同理,后面的位数也是按照这样子进行操作。
代码如下:
- #include <iostream>
- #include <vector>
- #include <algorithm>
-
- using namespace std;
-
- vector<int> div(vector<int> &A,int b,int r)
- {
- vector<int> c; //商
- r=0; //余数的引用
- for(int i=A.size()-1;i>=0;i--)
- {
- r = r*10+A[i];
- c.push_back(r/b);
- r%=b;
- }
-
- //与前面存的顺序是反过来的
- reverse(c.begin(),c.end());
- while(c.size()>1 && c.back()==0) c.pop_back();
-
- return c;
- }
-
- int main()
- {
- string a;
- int b;
-
- cin >> a >> b;
-
- vector<int> A;
- for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
-
- int r;
- auto c=div(A,b,r);
-
- for(int i=c.size()-1;i>=0;i--) printf("%d",c[i]);
- cout<<endl<<r<<endl;
-
- return 0;
- }
一、一维的前缀和
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1≤l≤r≤n, 1≤n,m≤100000 −1000≤数列中元素的值≤1000
输入样例:
5 3 2 1 3 6 4 1 2 1 3 2 4输出样例:
3 6 10
什么是前缀和?
Si=a1+a2+a3....+ai。令S0=0。其实就像以前所学的等差数列是差不多的道理。将S0定为0可以方便地处理边界。,当要求s[10]时,可以使用s10-s0=s10。要求某个区间内的和,直接sl-sr。
步骤:
求出每一个si的值,公式为s[i]=s[i-1]+a[i]。
之后再根据区间去求出
- #include <iostream>
-
- using namespace std;
-
- const int N = 100010;
-
- int n,m;
- int a[N],s[N];
-
- int main()
- {
- scanf("%d%d",&n,&m);
- for(int i=1;i<=n;i++) scanf("%d",&a[i]);
-
- for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
-
- while(m--)
- {
- int l,r;
- scanf("%d%d",&l,&r);
- printf("%d\n",s[r]-s[l-1]);
- }
- return 0;
- }
拓展:差分运算
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。
输出格式
共一行,包含 n 个整数,表示最终序列。
数据范围
1≤n,m≤100000 1≤l≤r≤n, −1000≤c≤1000
−1000≤整数序列中元素的值≤1000
输入样例:
6 3 1 2 2 1 2 1 1 3 1 3 5 1 1 6 1输出样例:
3 4 5 3 4 2
差分可以看作为前缀和的逆运算,前缀和是不断的求和数组,而对于差分可以说是将求和好的数组,一个一个拆解出来。
那么我们首先要定义一个原数组a:
a[1],a[2],a[3]......a[n];
然后我们构造一个数组b:
b[1],b[2],b[3].....b[n];
最后我们要使得a[i]=b[1]+b[2]+b[3]+.....+b[i];
也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话来说,其实每一个a[i]都是b数组从头开始的一段区间和。
那么如何构造这个数组呢?最直接的方法如下,其实跟我们以前学的数学是差不多的:
我们只要有b数组,通过前缀和运算,就可以在O(n)的时间内得到a数组。即 a[l] + c , a[l+1] + c , a[l+2] + c .... a[r] + c;
那么知道差分数组到底有什么用呢?
如果给定一个区间[l,r],让我们把a数组中的[l,r]区间中的每一个数都加上c。也可以暴力求解,for循环l到r的区间,时间复杂度为O(n),如果我们需要对原数组执行m次这样的操作,考虑时间复杂度就会变成O(n*m)。这样确实会浪费很多时间,那么,我们就需要考虑差分的做法:
我们知道a数组是b数组的前缀和数组,比如说对b数组的b[i]修改,会影响到a数组中从a[i]及往后的每一个数。
首先让差分b数组中的 b[l] + c ,a数组变成 a[l] + c ,a[l+1] + c.... a[n] + c;
然后我们打个补丁,b[r+1] - c, a数组变成 a[r+1] - c,a[r+2] - c....a[n] - c;
为什么要打个补丁?我们可以从图中得到:
b[l] + c,效果使得a数组中 a[l]及以后的数都加上了c(红色部分),但我们只要求l到r区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。
因此我们就可以得到一维差分的结论:
给a数组中的[l,r]区间中的每一个数都加上c,只需要对差分数组b做b[l]+=c,b[r+1]-=c。时间复杂度为O(1),提高了效率 。那么其实思路就如下了,很简单:
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
代码:
- #include <iostream>
-
- using namespace std;
-
- const int N=1e5+10;
- int a[N],b[N];
-
- int main()
- {
- int n,m;
- scanf("%d%d",&n,&m);
- for(int i=1;i<=n;i++)
- {
- scanf("%d",&a[i]);
- b[i]=a[i]-a[i-1]; //构建差分数组
- }
- int l,r,c;
- while(m--)
- {
- scanf("%d%d%d",&l,&r,&c);
- b[l] += c;
- b[r+1] -= c;
- }
- for(int i=1;i<=n;i++)
- {
- a[i] = b[i]+a[i-1];
- printf("%d",a[i]);
- }
- return 0;
- }
二、矩阵的前缀和
输入一个 n 行 m 列的整数矩阵,再输入 q个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数 n,m,q
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。
输出格式
共 q 行,每行输出一个询问的结果。
数据范围
1≤n,m≤1000 1≤q≤200000 1≤x1≤x2≤n 1≤y1≤y2≤m −1000≤矩阵内元素的值≤1000
输入样例
3 4 3 1 7 2 4 3 6 2 8 2 1 2 3 1 1 2 2 2 1 3 4 1 3 3 4输出样例
17 27 21
对于这种求二维数组的子矩阵和可以将数组看作一副图像的面积,那么对于一个矩阵的面积可以这么看:
紫色的面积代表的是(1,1)左上角到(i,j-1)右下角的矩形面积,绿色面积代表的是(1,1)左上角到(i-1,j)右下角的矩形面积。每一个颜色的矩形面积都代表了它所包围元素的和。
那么对于一个矩阵的面积可以这么看,整个外围蓝色矩阵的面积s[i] [j]=绿色面积s[i-1] [j]+紫色面积s[i] [j-1]-重复加的红色的面积s[i-1] [j-1]+小方块面积a[i] [j]。
那么就可以得出一条求其子矩阵面积的公式了:
s[i] [j] = s[i-1] [j]+s[i]s[j-1]+a[i] [j]-s[i-1] [j-1]
接下来的回归问题去求以(x1,y1)为左上角和以(x2,y2)为右下角的矩阵的元素的和。我们需要理清楚的是,其中的x1表示的行,而y1表示的是为列。
那么我们可以得出,如下:
紫色面积是指 ( 1,1 )左上角到(x1-1,y2)右下角的矩形面积 ,黄色面积是指(1,1)左上角到(x2,y1-1)右下角的矩形面积。
那么我们可以推出:
绿色矩形的面积 = 整个外围面积s[x2, y2] - 黄色面积s[x2, y1 - 1] - 紫色面积s[x1 - 1, y2] + 重复减去的红色面积 s[x1 - 1, y1 - 1]。
那么二位前缀和的结论就如下:
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]。
那么最后敲代码:
- #include <iostream>
-
- using namespace std;
-
- const int N=1010;
- int n,m,q;
- int a[N][N],s[N][N];
- int main()
- {
- scanf("%d %d %d",&n,&m,&q);
-
- //要确保从1开始,保证s[0][0]=0
- for(int i=1;i<=n;i++)
- {
- for(int j=1;j<=m;j++)
- {
- scanf("%d",&a[i][j]);
- }
- }
- for(int i=1;i<=n;i++)
- {
- for(int j=1;j<=m;j++)
- {
- s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1];
- }
- }
-
- while(q--)
- {
- int x1,y1,x2,y2;
- scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
- printf("%d\n",s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);
- }
- return 0;
- }
拓展:差分矩阵
输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)(x1,y1) 和 (x2,y2)(x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 c。
请你将进行完所有操作后的矩阵输出。
输入格式
第一行包含整数 n,m,q。
接下来 n行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含 5个整数 x1,y1,x2,y2,c,表示一个操作。
输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。
数据范围
1≤n,m≤1000, 1≤q≤100000, 1≤x1≤x2≤n, 1≤y1≤y2≤m, −1000≤c≤1000, −1000≤矩阵内元素的值≤1000
输入样例:
3 4 3 1 2 2 1 3 2 2 1 1 1 1 1 1 1 2 2 1 1 3 2 3 2 3 1 3 4 1输出样例:
2 3 4 1 4 3 4 1 2 2 2 2
像一维差分那样,我们想实现的是一种O(1)的时间复杂度,给子矩阵中每个元素的值加上c。
同理,a[] []是数组b[] []数组的前缀和数组,那么b[] []是a[] []的差分数组。原数组:a[i] [j],我们去构造差分数组b[i] [j]。其中a数组中a[i] [j]是b数组左上角(1,1)到右下角(i,j)所包围的矩形元素的和。
那么如何去构造b数组呢?同一维差分,我们构造二维差分数组目的是为了 让原二维数组a中所选中子矩阵中的每一个元素加上c的操作,可以由O(n*n)的时间复杂度优化成O(1)
已知原数组a中被选中的子矩阵为 以(x1,y1)为左上角,以(x2,y2)为右下角所围成的矩形区域;
也是跟前面一样,我们要记得a数组是b数组的前缀和数组,比如对b数组的修改,那么我们就会影响到从a[i] [j]以及往后的每一个数。
那么假设我们已经构造了b数组,类比一维差分,我们执行下面的操作来使被选中的子矩阵中的每个元素的值都加上c:
b[x1] [y1] + = c;
b[x1,] [y2+1] - = c;
b[x2+1] [y1] - = c;
b[x2+1] [y2+1] + = c;
那么每次都对b数组执行以上的操作,等价于:
- for(int i=x1;i<=x2;i++)
- for(int j=y1;j<=y2;j++)
- a[i][j]+=c;
我们使用图来表述可能会好理解一点:
b[x1] [ y1 ] +=c ; 对应图1 ,让整个a数组中蓝色矩形面积的元素都加上了c。
b[x1,] [y2+1]-=c ; 对应图2 ,让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1] [y1]- =c ; 对应图3 ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1] [y2+1]+=c; 对应图4,,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,才能使其恢复。
那么我们将上述所表达的封装为一个函数:
- void insert(int x1,int y1,int x2,int y2,c)
- {
- //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
- b[x1][y1] += c;
- b[x2+1][y1] -= c;
- b[x1][y2+1] -= c;
- b[x2+1][y2+1] += c;
- }
那么如何构建差分呢?我们每次让以(i,j)为左上角到以(i,j)为右下角面积内元素,其实就是一个小方格的面积去插入c=a[i] [j],那么这样就等价于原数组a中的(i,j)到(i,j)的范围内加上了a[i] [j],那么按照这么执行下去,进行n'*m的插入操作,就可以成功构建了差分b数组。
- for(int i=1;i<=n;i++)
- {
- for(int j=1;j<=m;j++)
- {
- insert(i,j,i,j,a[i][j]); //构建差分数组
- }
- }
代码如下:
- #include <iostream>
-
- using namespace std;
-
- const int N=1e3+10;
- int a[N][N],b[N][N];
-
- void insert(int x1,int y1,int x2,int y2,int c)
- {
- b[x1][y1] += c;
- b[x2+1][y1] -= c;
- b[x1][y2+1] -= c;
- b[x2+1][y2+1] += c;
- }
-
- int main()
- {
- int n,m,q;
- cin>>n>>m>>q;
- for(int i=1;i<=n;i++)
- for(int j=1;j<=m;j++)
- cin>>a[i][j];
- for(int i=1;i<=n;i++)
- for(int j=1;j<=m;j++)
- insert(i,j,i,j,a[i][j]);
- while(q--)
- {
- int x1,y1,x2,y2,c;
- cin>>x1>>y1>>x2>>y2>>c;
- insert(x1,y1,x2,y2,c);
- }
- for(int i=1;i<=n;i++)
- for(int j=1;j<=m;j++)
- b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]; //二维前缀和
- for(int i=1;i<=n;i++)
- {
- for(int j=1;j<=m;j++)
- {
- printf("%d ",b[i][j]);
- }
- printf("\n");
- }
-
- return 0;
- }
这种常见的问题分类:
对于一个序列,用两个指针维护一段区间。
对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
一、最长连续不重复子序列:
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n 个整数(均在 0∼105 范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
1≤n≤10^5
输入样例:
5 1 2 2 3 5输出样例:
3
指定两个指针 ,一个是指向元素i的指针,另外一个是指向最长连续不重复的子序列j。之后我们就可以得知长度为i-j+1,将这一长度与r的较大者更新给r。
对于每一个i,如何确定j的值呢,由于[j,i-1]提前一步得到的最长连续不重复子序列,所以如果[j,i]中有重复的元素,那么一定是a[i],那我们就将j指针一直移动直到不重复为止。(由于[j,i-1]已经是前一步的最优解,此时只可能剔除重复元素a[i],不可能左移增加元素,所以j这个指针式具有单调性)。
用数组s记录子序列中a[j~i]各元素出现次数,遍历过程中对于每一个i有四步操作:cin元素[i]->将a[i]出现次数s[a[i]]加一->若a[i]重复则右移j(s[a[j]]要减一)->确定j以及更新当前长度i-j+1给r。
需要注意的细节是:当a[i]重复时,先把a[j]次数减1,再右移j。
- #include <iostream>
-
- using namespace std;
-
- const int N = 100010;
- int a[N],s[N];
-
- int main()
- {
- int n,r=0;
- cin >> n;
-
- for(int i=0,j=0;i<n;++i)
- {
- cin>>a[i];
- ++s[a[i]];
- while(s[a[i]]>1) --s[a[j++]];//先减次数后右移j
- r=max(r,i-j+1);
- }
- cout << r;
-
- return 0;
- }
二、数组元素的目标和
给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x的数对 (i,j)。
数据保证有唯一解。
输入格式
第一行包含三个整数 n,m,x,分别表示 A的长度,B 的长度以及目标值 x。
第二行包含 n 个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。
输出格式
共一行,包含两个整数 i 和 j。
数据范围
数组长度不超过 10^5。 同一数组内元素各不相同。 1≤数组元素≤109
输入样例:
4 5 6 1 2 4 7 3 4 6 8 9输出样例:
1 1
可能很多人会想到这个暴力求解:
- #include <iostream>
-
- using namespace std;
-
- const int N=10010;
-
- int main()
- {
- int n=0,m=0,x=0;
- int a[N],b[N];
- scanf("%d%d%d",&n,&m,&x);
- for(int i=0;i<n;i++)
- {
- scanf("%d",&a[i]);
- }
- for(int j=0;j<m;j++)
- {
- scanf("%d",&b[j]);
- }
-
- for(int i=0;i<n;i++)
- {
- for(int j=0;j<m;j++)
- {
- if(a[i]+b[j]==x)
- {
- printf("%d %d",i ,j);
- }
- }
- }
-
- return 0;
- }
由题意可得,这是一个升序的数组,那么我们可以定义两个指针,第一个指针i从前往后遍历,一个指针j从m-1开始从后向前遍历。那么和纯暴力求解的区别就在于,我们的j指针不会回退,不会一直在重复遍历。当我们使用两个指针进行遍历时,如果存在这个时候有a[i]+b[j]>k,那么就说明,指针j需要往前移动。
- #include <iostream>
-
- using namespace std;
-
- const int N=1e5+10;
-
- int n,m,k;
- int a[N],b[N];
-
- #define read(x) scanf("%d",&x);
-
- int main()
- {
- read(n);read(m);read(k);
- for(int i=0;i<n;i++) read(a[i]);
- for(int i=0;i<m;i++) read(b[i]);
-
- for(int i=0,j=m-1;i<n;i++)
- {
- while(j>=0&&a[i]+b[j]>k) j--;
- if(j>=0&&a[i]+b[j]==k) printf("%d %d",i,j);
- }
- return 0;
- }
三、判断子序列
给定一个长度为 nn 的整数序列 a1,a2,…,ana1,a2,…,an 以及一个长度为 mm 的整数序列 b1,b2,…,bmb1,b2,…,bm。
请你判断 aa 序列是否为 bb 序列的子序列。
子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5}{a1,a3,a5} 是序列 {a1,a2,a3,a4,a5}{a1,a2,a3,a4,a5} 的一个子序列。
输入格式
第一行包含两个整数 n,m。
第二行包含 n 个整数,表示 a1,a2,…,ana1,a2,…,an。
第三行包含 m 个整数,表示 b1,b2,…,bmb1,b2,…,bm。
输出格式
如果 a 序列是 b 序列的子序列,输出一行
Yes
。否则,输出
No
。数据范围
1≤n≤m≤10^5 −10^9≤ai,bi≤10^9
输入样例:
3 5 1 3 5 1 2 3 4 5输出样例:
Yes
使用双指针算法:
使用j指针来扫描整个b数组,i指针用来扫描a数组。若发现a[i]=b[j],则让i指针后移一位。
整个过程中,j指针不断后移,而i指针只有当匹配成功时才后移一位,到最后如果i==n时,那就说明匹配成功
代码:
- #include <iostream>
-
- using namespace std;
- const int N=1e5+10;
- int a[N],b[N];
- int main()
- {
- int n,m;
- scanf("%d%d",&n,&m);
- for(int i=0;i<n;i++) scanf("%d",&a[i]);
- for(int j=0;j<m;j++) scanf("%d",&b[j]);
-
- int i=0;
- for(int j=0;j<m;j++)
- {
- if(i<n&&a[i]==b[j]) i++;
- }
- if(i==n) puts("Yes");
- else puts("No");
-
- return 0;
- }
题目:二进制中1的个数
给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。
输入格式
第一行包含整数 n。
第二行包含 n 个整数,表示整个数列。
输出格式
共一行,包含 n 个整数,其中的第 i 个数表示数列中的第 i 个数的二进制表示中 11的个数。
数据范围
1≤n≤100000, 0≤数列中元素的值≤109
输入样例:
5 1 2 3 4 5输出样例:
1 1 2 1 2
我们先思考如何得到一个数的二进制数:
- int n=10;
- for(int k=3;k>=0;k--) cout<<(n>>k&1);
那么上面就是一种思路:
先把第k位移到最后一位 n>>k,再看个位是几,那就要进行与上1。
lowbit原理
作用是返回x的最后一位1。比如说x=1010,那么lowbit(x)=10,如果x=101000,那么返回的是1000。
这涉及到了原码、补码和反码,当我们一个负数要存储时,我们正常是先将它对应的正数取反后再加上1。那么lowbit其实就可以表达为(x&(~x+1))。比如说一个数字的原码是10001000,它的负数形式表达就是补码,那就是反码+1,反码是01110111,加1则是01111000,二者按位与得到了1000。应用就是可以求出二进制中1的个数。
那么代码如下:
- #include <iostream>
-
- using namespace std;
-
- int lowbit(int x)
- {
- return x & -x;
- }
-
- int main()
- {
- int n;
- cin>>n;
- int res=0;
- while(n--)
- {
- int x;
- cin >> x;
-
- int res=0;
- while(x) x-=lowbit(x),res++; //计算一共减去了多少次1
- cout << res << ' ' ;
- }
- return 0;
- }
其实也可以暴力求解:
这个思路就很简单了,就像上面的得到了每个数的最后一位,之后就将a右移一位,直到位等于0,就得到了1的个数。
- #include <iostream>
-
- using namespace std;
-
- int n;
- int a,k;
- int main()
- {
- scanf("%d",&n);
-
- for(int i=0;i<n;i++)
- {
- scanf("%d",&a);
- k=0;
- while(a)
- {
- k += a&1;
- a=a>>1;
- }
- printf("%d",k);
- }
- return 0;
- }
先看看题目:区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
−10^9≤x≤10^9 1≤n,m≤10^5 −10^9≤l≤r≤10^9 −10000≤c≤10000
输入数据:
3 3 1 2 3 6 7 5 1 3 4 6 7 8输出样例:
8 0 5
首先为什么要实现离散化?
存储下标太大了,开这么大的数组不是很现实。
数轴中存在负数,如果采用数组下标,那就是会存在负值,这不可能。
那能不能用哈希?哈希无法像离散化一样缩小数组空间,导致我们需要从-1e9到1e9,需要从前到后去枚举。
哈希表也无法实现排序,所以我们一般不能提前直到哪些数轴上的点存在哪些不存在,所以一般是从负的最小值到正的最大值都枚举一遍,那么这样时间复杂度太高了。
离散化的本质,就是映射,将间隔很大的点,映射到相邻的数组元素中,减少对空间的需求,也减少计算量。
那么我们先看看思路:
读输入:将每次读入的x c,使用push_back()到add中,将每次读入的位置x,push_back()到alls中,将每次读入的l r ,使用push_back()到query中。
排序、去重。
通过遍历add,完成在离散化的数组映射到a数组上进行加上c的操作,使用find函数。
初始化s数组。
通过数组query,完成求区间[l,r]的和。
先看看代码:
- #include <iostream>
- #include <vector>
- #include <algorithm>
-
- using namespace std;
-
- typedef pair<int,int> PII;
- const int N=30010;
-
- int n,m;
- int a[N],s[N];
-
- vector<int> alls;
- vector<PII> add,query;
-
- int find(int x)
- {
- int l=0,r=alls.size()-1;
- while(l<r)
- {
- int mid=l+r>>1;
- if(alls[mid]>=x) r=mid;
- else l=mid+1;
- }
- return r+1;
- }
-
- //实现c++中的unique去重函数
- vector<int>::iterator unique(vector<int> &a)
- {
- int j=0;
- for(int i=0;i<a.size();i++)
- if(!i || a[i]!=a[i-1])
- a[j++]=a[i];
- //a[0]~a[j-1]所有a中不重复的数
- return a.begin()+j;
- }
-
- int main()
- {
- cin >> n >> m;
- for(int i=0;i<n;i++)
- {
- int x,c;
- cin >> x >> c;
- add.push_back({x,c});
- alls.push_back(x);
- }
- for(int i=0;i<m;i++)
- {
- int l,r;
- cin >> l >> r;
- query.push_back({l,r});
- alls.push_back(l);
- alls.push_back(r);
- }
-
- //去重以及排序
- sort(alls.begin(),alls.end());
- alls.erase(unique(alls),alls.end());
-
- //处理插入
- for(auto item:add)
- {
- //找到下标
- int x=fine(item.frist);
- //插入下标对应的值
- a[x] += item.second;
- }
-
- //预处理前缀和
- for(int i=);i<=alls,size();i++) s[i]=s[i-1]+a[i];
-
- //处理访问
- for(auto item:query)
- {
- int l=find(item.first),r=find(item.second);
- cout<<s[r]-s[l-1]<<endl;
- }
- return 0;
- }
那么首先我们应该确定的是,我们在alls中存放的是位置而不是值,也就是存放的是x而不是c。
所以我们需要alls.push_back(l)\alls.push_back(r)去保存我们所要询问的值的位置。
在求区间和时,我们提前分析到可以使用前缀和来做,求前缀和就需要下标l r,如果不加入l r到alls中的话,第五步中遍历query时就没有办法通过l r去访问a或者s。因为find函数就是输入映射前的下标,返回alls中的下标+1。
那么为什么要去重还有排序?
对于一个要进行二分的离散数组,是需要进行排序的。find函数的功能就是在一个排好序的数组下,输入一个离散数组位置(映射前的位置)x返回连续数组的位置+1也就是映射后的位置+1。+1的目的是在于求区间和时少一步下标为0的判断。
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3] 和 [2,6] 可以合并为一个区间[1,6]。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含两个整数 l 和 r。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
1≤n≤100000
−10^9≤li≤ri≤10^9
输入样例
5 1 2 2 4 5 6 7 8 7 9输出样例:
3
快速将具有交集的区间合并为一个区间。区间与区间的关系:
包含
交集
无关
按照左端点从小到大的顺序进行遍历(这样就不会有区间会出现左端起点位置会小于下一个要遍历区间的起始位置)。比如我们输入了一个区间,还有另外一个区间,如果这个区间有交集,那么我们就把这个区间的结尾存放在后面,如果这个区间完全没有交集,那么就有当前起点定为下一个区间的起点。
那么就看看代码:
- #include <iostream>
- #include <algorithm>
- #include <vector>
-
- using namespace std;
-
- typedef pair<int,int> PII;
- const int N=100010;
-
- int n;
- vector<PII> segs;
-
- void merge(vector<PII> &segs)
- {
- vector<PII> res;
- //排序
- sort(segs.begin(),segs.end());
- //定义左右边界
- int st=-2e9,ed=-2e9;
- for(auto seg:segs)
- {
- //说明我们的区间里面没有这个,需要外扩
- if(ed<seg.first)
- {
- if(st!=-2e9) res.push_back({st,ed});
- st = seg.first,ed=seg.second;
- }
- //包含区间
- else
- {
- ed = max(ed,seg.second);
- }
- }
- if(st!=-2e9) res.push_back({st,ed});
-
- segs = res;
- }
-
- int main()
- {
- cin>>n;
- for(int i=0;i<n;i++)
- {
- int l,r;
- cin >> l >> r;
- segs.push_back((l,r));
- }
- merge(segs);
-
- cout<<segs.size()<<endl;
-
- return 0;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。