文章目录
一.排序
(1)快速排序
const int N = 1e6 + 10;
int a[N], n;//从0开始
void quick_sort(int l, int r)
{
int i = l, j = r;//二分思想
int mid = a[(l + r) / 2];
do{
while(a[i] < mid)//严格小于,i指针才往右移动
++ i;
while(a[j] > mid)//严格大于,j指针才往左移动
-- j;
if(i <= j){//双指针都要记得判断边界
swap(a[i], a[j]);//交换后i,j继续移动
++ i;
-- j;
}
}while(i <= j);//如果i<=j则继续移动,结束后会被分为三个区间,[l, j], x, [i, r]
if(j > l)//当i>j时才会退出上述循环进行下一步的操作
quick_sort(l, j);//分治,递归地排序
if(i < r)
quick_sort(i, r);
}
quick_sort(0, n - 1);
(2)归并排序
void merge_sort(int a[],int left,int right)//本来想用vector的,结果gg,QAQ
{
if(left>=right)//递归终止条件
return;
int mid=(left+right)>>1;
merge_sort(a,left,mid);//左右区间分别进行递归,最后得到的两个区间都各自有序
merge_sort(a,mid+1,right);
int i=left,j=mid+1;//双指针,分别从两个区间的起点开始
int cnt=0;
while(i<=mid && j<=right){ //合并两个区间
if(a[i]<=a[j])
temp[cnt++]=a[i++];
else
temp[cnt++]=a[j++];
}
while(i<=mid)
temp[cnt++]=a[i++];
while(j<=right)
temp[cnt++]=a[j++];
for(int i=left,j=0;i<=right;++i,++j) //关键步骤,把某个递归后的结果返还回去,不注意这里会很惨
a[i]=temp[j];
}
merge_sort(a,0,n-1); //n个数
二.二分法
1.整数二分:
整数二分有两种情况,一种是目标数在右区间,另一种是目标数在左区间,因此有两种板子;(k即为target,l为左边界left,r为右边界right)
重点看第3,4部分题目的check函数下面部分的边界情况考虑,稍微想想就行,思考哪些是可取的,哪些是不可取的,还有右边的怎么样,左边的怎么样,如当前值可取,右边值也可取,则left=mid,当前值不可取,右边值也不可取,则right=mid-1,当前值不可取,右边值可取,则left=mid+1,这样根据问题思考后再套模板即可轻松求解二分(因为[l,r]中都是可能取值,包括l,r,一旦离开该区间,则说明为不可行解);
(1)第一种板子
int search_1(int l,int r)
{ //二分时首先确定目标是在左区间还是在右区间,要提前想好,关键
while(l<r){
int mid=(l+r)>>1;//由下面的推出mid,若r=mid,则mid=(l+r)>>1
if(a[mid]>=k)//关键,目标为大于等于某个数,在右区间,所以直接根据题目要求使得a[mid]>=k即可
r=mid; //因为是求大于等于某个数中的最小值,所以直接缩小范围,只要求最小的即可,在mid右边的肯定大于等于mid,所以mid就够了
else
l=mid+1;
}
}
(2)第二种板子
int search_2(int l,int r)
{
while(l<r){
int mid=(l+r+1)>>1;//由下面的推出mid,若l=mid,则mid=(l+r+1)>>1
if(a[mid]<=k)//关键,目标为小于等于某个数,在左区间,所以直接根据题目要求使得a[mid]<=k即可
l=mid; //因为是求小于等于某个数中的最大值,所以直接缩小范围,只要求最大的即可,在mid左边的肯定都小于等于mid,所以mid在[,mid]中是最大的,故取mid
else
r=mid-1;
}
}
若查找失败,则
if(a[l]!=k)
printf("无目标数\n");
2.浮点数二分:
浮点数二分比整数二分简单,因为不需要考虑边界;
bool check(double x)
{/* ... */} // 检查x是否满足某种性质
double search_3(double l, double r)
{
const double eps = 1e-6; // 保留四位小数为1e-6,保留五位小数为1e-7,保留六位小数为1e-8
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid; //浮点数二分不考虑边界,所以l=mid即可
}
return l;
}
3.跳石头(非常好的一道理解二分的题目!)
题目链接
二分答案,挺有意思的一道题;
正常去想是不行的,要反其道而行之,去枚举答案距离,再用该距离去计算需要移除的石头数,如果石头数超过题目所给,说明该距离太大了,要缩小该距离,如果石头数符合,由于是求max(最短跳跃距离),所以可以先记录一下当前距离,然后放大距离,看是不是也符合要求,这样就可以去用二分枚举答案距离;
如何计算需要移除的石头数:计算当前所在石头与下一块石头的距离,如果这个距离小了,又由于所假设的距离就是答案距离,所以该石头需要被移除;
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 5e4 + 10;
int a[N];
int len, n, m;
bool check(int k)//前提假设k就是正确答案,即k为max(最短跳跃距离),小于k的跳跃距离涉及的石头都要被拿掉
{
int cnt = 0;
int s = 0;//当前石头与起点的距离
for(int i = 1; i <= n + 1; ++ i){
if(a[i] - s >= k)//a[i]为当前石头的下一块石头与起点的位置,两者之差即为跳跃距离(两石头距离)
s = a[i];//如果大于等于k,则说明当前跳跃距离不小于当前最短跳跃距离,不移走该石头,更新当前所在石头与起点距离
else
cnt ++;//如果小于k,则不符合前提假设,拿掉该石头
}
if(cnt > m)//统计拿掉的石头,如果比题目要求的还多,说明此k为不可行解
return false;
else
return true;
}
int main()
{
cin >> len >> n >> m;
for(int i = 1; i <= n; ++ i)
scanf("%d", &a[i]);
a[n + 1] = len;
int l = 1, r = len;
while(l < r){
int mid = (l + r + 1) >> 1;//要理解[l,r]中的l,r都是可能的取值,由r=mid-1推出该式要+1
if(check(mid)){//因为mid是可行解,说明mid后面可能有更好的解,因为是求的max(最短跳跃距离),解在右边
l = mid;//可行就保留,即l=mid
}
else//如果mid不可行,说明跳跃距离太大了,太多石头被拿了,如果mid更大,那么将有更多的石头被拿,所以要缩小mid的值
r = mid - 1;//不可行就移除,即r=mid-1,这样mid-1就再也不会被考虑了
}
cout << l << endl;
return 0;
}
4.砍树(也是一道帮助理解二分的题目)
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
ll a[N];
int main()
{
ll n, m;
ll maxn = 0;
cin >> n >> m;
for(int i = 0; i < n; ++ i) {
scanf("%lld", &a[i]);
maxn = max(maxn, a[i]);
}
ll l = 1, r = maxn;
ll mid;
while(l < r){
ll res = 0;
mid = (l + r + 1) >> 1;
for(int i = 0; i < n; ++ i)
if(a[i] > mid)
res += a[i] - mid;
if(res < m)//考虑边界情况,如果res小了,说明这个mid不可取,那么右边的mid更加不可取,所以r=mid-1
r = mid - 1;
else
l = mid;//如果是大于等于,说明这个mid可取,也许后面的也行,所以是l=mid
}
cout << l << endl;
return 0;
}
三.大数高精度处理
大数基本处理:
string a;
cin>>a;
vector<int> x;
for(int i=a.size()-1;i>=0;--i)//倒着存,即个位在首位,高位在后面,为了方便进位的处理,如果最高位在首位那么进位处理就比较麻烦了,所以最高位在vector的最右边
x.push_back(a[i]-'0');
auto c=func(x,b);
for(int i=c.size()-1;i>=0;--i)
printf("%d",c[i]);
1.大数加法:
vector<int> add(vector<int>& x,vector<int>& y) //大数高精度加法
{
vector<int> ans;
int t=0;
for(int i=0;i<x.size() || i<y.size();++i){ //t=x[i]+y[i]+ci,进位ci即为之前的t
if(i<x.size())
t+=x[i];
if(i<y.size())
t+=y[i];
ans.push_back(t%10);
t/=10; //中间过程的进位
}
if(t) //若最高位有进位则加上
ans.push_back(t);
return ans;
}
2.大数减法
vector<int> sub(vector<int>& x,vector<int>& y)
{
vector<int> ans;
int t=0;
for(int i=0;i<x.size();++i){ //ans[i]=x[i]-y[i]-ci
t=x[i]-t; //减去进位
if(i<y.size())
t-=y[i];
ans.push_back((t+10)%10); //包含了两种情况,一是t>0,加10模10后仍为t本身,二是t<0,借位后模10
if(t<0) //更新进位
t=1;
else
t=0;
}
while(ans.size()>1 && ans.back()==0) //去除前导0
ans.pop_back();
return ans;
}
减法还需考虑x-y<0的情况
bool cmp(vector<int>& x,vector<int>& y) //比较两个数的大小,若x>=y则返回true,否则返回false
{
if(x.size() != y.size()) //先比较位数
return x.size() > y.size();
for(int i=x.size()-1;i>=0;--i){ //在位数相同的情况下依次从高位向低位进行比较
if(x[i]!=y[i])
return x[i]>y[i];
}
return true;
}
if(cmp(x,y)){ //保证传进sub函数的x>y
auto c=sub(x,y);
for(int i=c.size()-1;i>=0;--i)
printf("%d",c[i]);
cout<<endl;
}
else{ //若x<y,则x-y=-(y-x)
cout<<'-';
auto c=sub(y,x);
for(int i=c.size()-1;i>=0;--i)
printf("%d",c[i]);
cout<<endl;
}
3.大数乘法
(1)高精度*数
vector<int> mul(vector<int> & a, int b)
{
vector<int> ans;
int t = 0;
for(int i = 0; i < a.size(); ++ i){
t += a[i] * b;//ans[i] = a[i]*b+ci,将b看成是一个整体,使a的每一位与之相乘
ans.push_back(t % 10);//保留尾数
t /= 10;//进位
}
while(t){//和高精度加法一样,需要考虑最高位的进位
ans.push_back(t % 10);
t /= 10;
}
while(ans.size() > 1 && ans.back() == 0)//去除多余的前导0
ans.pop_back();
return ans;
}
(2)高精度*高精度
vector<int> mul(vector<int> & a, vector<int> & b)
{
vector<int> ans(a.size() + b.size(), 0);//将可能用到的部分都初始化成0,如999*99,3位数乘以2位数最多不会超过5位数
for(int i = 0; i < a.size(); ++ i)//不初始化的话会RE
for(int j = 0; j < b.size(); ++ j)//类似预处理了
ans[i + j] += a[i] * b[j];//手动模拟一下就挺好理解的,如65*34
int t = 0;
for(int i = 0; i < ans.size(); ++ i){//类似其他高精度
t += ans[i];
ans[i] = t % 10;
t /= 10;
}
while(ans.size() > 1 && ans.back() == 0)//去除前导0
ans.pop_back();
return ans;
}
4.大数除法
vector<int> div(vector<int>& a,int b,int& r) //传入r的引用,使得r在函数中变化后,也能在main函数中得到变化后的r
{
vector<int> ans;
r=0;
for(int i=a.size()-1;i>=0;--i){ //也是把b看成一个整体
r=r*10+a[i]; //ans[i]=(r*10+a[i])/10
ans.push_back(r/b);
r%=b;
}
reverse(ans.begin(),ans.end()); //因为此时正序即为商,但为了和外部接口保持一致,需要翻转一下
while(ans.size()>1 && ans.back()==0) //去除多余的前导0
ans.pop_back();
return ans;
}
模板题
总结:大数运算主要有以下几个步骤:
1.对输入的大数进行处理后倒序放入vector中;
2.在函数内部先令进位t或者余数r=0,然后根据加减乘除各个运算的特点,按位依次运算;
3.减乘除还需注意去除多余的前导0;
4.倒序输出vector;
四、前缀和及差分
1.前缀和
xtuOJ经常要用的一种思想(谢大出的题真的有点。。。)
可以把原本O(n)的查询时间降为O(1);
需要设置f[0]=0,f[0][0]=0等初始条件处理边界值,这样就不需要特判了。
(1)一维情况及最大子段和
给出区间[l,r],求其上的a[l]+a[l+1]+···+a[r],可转化为f[r]-f[l-1];
公式:
f[i]=f[i-1]+a[i]
f[0]=0
模板题
前缀和一般从1开始
最大子段和:给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大
2 -4 3 -1 2 -4 3
3 -1 2为最大子段和对应子段
状态转移方程: f [ i ] = m a x ( a [ i ] , a [ i ] + f [ i − 1 ] ) f[i] = max(a[i],a[i]+f[i-1]) f[i]=max(a[i],a[i]+f[i−1]) ( f [ i ] f[i] f[i]为到 i i i为止的的最大子段和)
(2)二维情况和二维最大子段和
子矩阵的和
基本公式:
s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j]
模板题
二维最大子段和(可求矩阵中任意大小的子矩阵,如求最大子矩阵之和)
题目链接
题解
重点:压缩矩阵
在使用动态规划求解序列(矩阵)连续最大和的过程中,答案ans记录的是从开始到当前位置的最大和,所以在遍历的时候ans永远是到当前位置为止已知的最大值。
二维矩阵求解的过程,要分三个部分进行求解,即i,k,j分别表示当前最下面的行(最下面的行是第i行),压缩行(即连续的k行(k=1,…,i),从压缩1行到压缩i行),和列(当行和压缩行确定后,就相当于是求一维的子段最大和了)
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N =130;
int a[N][N];
int ans;
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j) //重点是把二维的压缩成一维的进行处理
scanf("%d", &a[i][j]);
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j)
a[i][j] += a[i - 1][j];//压缩矩阵,就不用遍历行了,a[i][j]-a[i-k][j],i=5,k=3,就是压3行,
for(int i = 1; i <= n; ++ i){//即把第1,2行减去,剩下的就是3,4,5行,求3~5行的最大子矩阵,再遍历j(列)即可
for(int k = 1; k <= i; ++ k){//把以i为最下面一行的矩阵压k行!!!
int f[N] = {0};//这里就是类似一维最大子段和
for(int j = 1; j <= n; ++ j){//枚举每一列,就类似一维最大子段和从前往后遍历
f[j] = a[i][j] - a[i - k][j];//此时f[j]就是一维最大子段和中的每一个元素
f[j] = max(f[j], f[j] + f[j - 1]);//res[j]就是到j为止的的最大子段和
ans = max(ans, res[j]);//更新最大子矩阵答案
}
}
}
printf("%d\n", ans);
return 0;
}
2.差分
(1)一维差分
差分可以看做是前缀和的逆运算。
如已知f[i],需要求a[i],使得a[0]+a[1]+···+a[i]=f[i],a[i]就被称为f[i]的差分。
效果也是使得O(n)的操作变为O(1);
一种简单的构造方式:
a[0]=f[0]
a[1]=f[1]-f[0]
a[2]=f[2]-f[1]
···
a[i]=f[i]-f[i-1]
这样就可以保证a[0]+a[1]+···+a[i]=f[i]
应用
模板题
#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int main()
{
int n,m;
int l,r,c;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i) //从1到n的闭区间,为了使之后的差分构造保持一致
scanf("%d",&a[i]);
for(int i=1;i<=n;++i) //构造差分数组
b[i]=a[i]-a[i-1];
while(m--){
scanf("%d%d%d",&l,&r,&c);
b[l]+=c; //关键步骤,若b[l]加上c,则后面求前缀和时,f[l]及后面的元素都加上了c,即实现了要求
b[r+1]-=c; //b[r+1]减去c,使得b[r]后面的元素在求前缀和时不会加上c,即实现了只有[l,r]内的元素加上c
}
for(int i=1;i<=n;++i) //求前缀和,即返回答案数组
b[i]+=b[i-1]; //由a[i]=a[i-1]+b[i],把a改为b即可
for(int i=1;i<=n;++i)
cout<<b[i]<<' ';
cout<<endl;
return 0;
}
(2)二维差分
#include<iostream>
using namespace std;
const int N=1e3+10;
int a[N][N];
int b[N][N];
int main()
{
int n,m,q;
int x1,y1,x2,y2,c;
scanf("%d%d%d",&n,&m,&q);
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)
b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];//由s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j]反推出a[i][j]即差分
while(q--){
scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&c);
b[x1][y1]+=c; //(x1,y1)及其右下角的的数都加上c
b[x2+1][y1]-=c; //(x2,y1)下面的数都减去c,防止无关数受影响
b[x1][y2+1]-=c; //(x1,y2)右边的数都减去c,与上同理
b[x2+1][y2+1]+=c; //由于上两式,(x2,y2)右下角的数减了两次c,所以需要再加上一个c
}
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)//类比差分b[i]=f[i]-f[i-1],f[i]=b[i]+f[i-1],b[i]+=b[i-1]后,b[i]即为前缀和
b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];//则由b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]
for(int i=1;i<=n;++i) {//前缀和:a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j], 把a改为b即可, b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1]即为前缀和
for (int j = 1; j <= m; ++j)
printf("%d ", b[i][j]);
cout<<endl;
}
return 0;
}
五、双指针
双指针最大的作用就是优化。如双重for循环,暴力的话需要O(n^2),但是用双指针的话只要
2
∗
n
2*n
2∗n,即O(n),显著降低了时间复杂度。双指针也是如快排,归并等算法实现的关键。
做题时可以先写出O(n^2)的暴力写法,然后找出i,j间的关系以及单调性,再进行优化,变成
2
∗
n
2*n
2∗n即O(n)的双指针算法。
经典例题:
给定一个长度为n的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数n。
第二行包含n个整数(均在0~100000范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
1≤n≤100000
输入样例:
5
1 2 2 3 5
输出样例:
3
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int a[N];
int s[N];
int main()
{
int n;
int ans=0;
cin>>n;
for(int i=0;i<n;++i)
scanf("%d",&a[i]);
for(int i=0,j=0;i<n;++i){ //i,j只能向右移动,当[j,i]为当前最大不重复区间时,由于i只能向右移动,此时[j-1,i+1]一定不会为不重复区间,[j,i+1]可能为重复区间,因为一旦[j-1,i+1]为不重复区间,则[j-1,i]也为不重复区间,与[j,i]为最大重复区间矛盾,即j不能左移,只能右移
s[a[i]]++; //记录a[i]出现的次数
while(s[a[i]]>1){ //关键步骤,当出现重复元素时,维护区间[i,j]
--s[a[j]]; //更新,将重复元素之前的值全部清零,使得只有当前元素(重复)(新区间起点)的出现次数为1,并且协助j的移动
++j; //向右移动j,最后达到新区间的起点
}
ans=max(ans,i-j+1); //i到j的长度为i-j+1,更新
}
cout<<ans<<endl;
return 0;
}
1.第一类用法
对于一个序列,用两个指针维护一段区间;
例题:去除多余空格
输入一个字符串,字符串中可能包含多个连续的空格,请将多余的空格去掉,只留下一个空格。
输入格式
共一行,包含一个字符串。
输出格式
输出去掉多余空格后的字符串,占一行。
数据范围
输入字符串的长度不超过200。
输入样例:
Hello world.This is c language.
输出样例:
Hello world.This is c language.
#include <iostream>
using namespace std;
int main()
{
string str, res;
getline(cin, str);
for(int i = 0;i < str.size(); ++ i){
if(str[i] == ' '){
int j = i + 1;
while(j < str.size() && str[j] == ' ') ++ j;
i = j - 1;
}
res.push_back(str[i]);
}
for(int i = 0;i < res.size(); ++ i) printf("%c", res[i]);
return 0;
}
输入一个整数数组,实现一个函数来调整该数组中数字的顺序。
使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。
样例
输入:[1,2,3,4,5]
输出: [1,3,5,2,4]
class Solution {
public:
void reOrderArray(vector<int> &array) {
int i=0,j=array.size()-1;
while(i<j){
while(i<j && array[i]%2==1)
++i;
while(i<j && array[j]%2==0)
--j;
if(i<j)
swap(array[i],array[j]);
}
}
};
2.第二类用法
对于两个序列,维护某种次序;
如归并排序中的区间合并;
六、位运算
lowbit算法:
a&(-a)=最右的1以及后面的0
如3&(-3)=1 (3二进制为11,最右边1以及后面的数就是1)
10&(-10)=10 (10二进制为1010,最右边的1以及后面的数为10)
利用lowbit算法可以求解一个数中转化成二进制后1的个数。
模板题
#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N];
int main()
{
int n;
cin>>n;
for(int i=0;i<n;++i) {
scanf("%d",&a[i]);
int num=0;
while(a[i]){
if(a[i]&(-a[i])){
num++;
a[i]-=a[i]&(-a[i]); //一步步去除有1的部分
}
}
cout<<num<<" ";
}
cout<<endl;
return 0;
}
七、离散化
离散化是一种非常特殊的hash方式,保序;
当题目给出的数据值范围量很大,但是实际上用到的数据数量不多时使用,将规模比较大的数转换成规模比较小的数;
基本思想:把所有要用到的数先全部放到一个数组(vector)中,而不需要考虑其数值范围,然后进行排序,去重,再通过一个find(二分查找)函数对其进行映射,将原本可能很大的数的存在范围映射成很小的范围,如1,1000,2000000变成1,2,3,即用下标(连续)来表示原来的值(要和hash区分开,hash不能排序);
> 重点:
1.读入所有数据到数组中;
2.排序,去重后得到有序不重复的基本数组x;
3.find通过二分查找将数据值根据其在x中的位置映射成从0开始或从1开始的下标;
4.最后进行对应的计算操作;
基本模板
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
//return lower_bound(alls.begin(),alls.end,x) - all.begin + 1; //或者直接用lower_bound二分函数返回数组下标
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; // 映射到1, 2, ...n
}
模板题区间和:
假定有一个无限长的数轴,数轴上每个坐标上的数都是0。 现在,我们首先进行 n 次操作,每次操作将某一位置x上的数加c。 接下来,进行 m
次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和。 输入格式 第一行包含两个整数n和m。 接下来 n
行,每行包含两个整数x和c。 再接下里 m 行,每行包含两个整数l和r。 输出格式 共m行,每行输出一个询问中所求的区间内数字和。 数据范围
−109≤x≤109, 1≤n,m≤105, −109≤l≤r≤109, −10000≤c≤10000 输入样例: 3 3 1 2 3 6
7 5 1 3 4 6 7 8 输出样例: 8 0 5
思路:易知需要用到前缀和,但由于数值范围是1e9,数组最多容量为1e8,且用到的数据数量只有3e5,所以要用离散化缩小数值范围:把需要用到的三种坐标:x,l,r放入vector数组中进行离散化排序去重得到有序无重复vector数组pos,然后再借助二分查找find函数将读入的要做加法操作以及查询操作的坐标根据其在pos中的位置映射到1~n(前缀和下标从1开始),即做离散化操作,然后再进行对应的加法或求前缀和;
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N=3e5+10; //由于加法操作给出的坐标最多是1e5个,查询操作给出的坐标最多是2e5个,所以最大为3e5个
typedef pair<int,int> pii; //二元组,存储<x,c>,<l,r>,即类似结构体,将两个变量关联起来
int a[N]; //区间点数组
int f[N]; //前缀和数组,f[i] = f[i-1] + a[i]
vector<pii> add,query; //加法操作和查询操作都涉及两个有关联的变量
vector<int> pos; //记录需要用到的位置坐标的数组,x,l,c都会被放入该数组
//为了方便使用前缀和,数组下标都从1开始
int find(int x) //关键函数,通过其实现离散化
{
//return lower_bound(pos.begin(),pos.end(),x)-pos.begin() + 1; //lower_bound二分查找,返回对应元素下标
int l = 0,r = pos.size() - 1;
while(l < r){
int mid = (l + r) >> 1;
if(pos[mid] >= x)
r = mid;
else
l = mid + 1;
}
return r+1;
}
vector<int>::iterator uni(vector<int>& a) //手写unique去重函数
{
int j = 0;
for(int i = 0; i < a.size(); ++i){
while(!i || a[i] != a[i-1]) //不会重复的条件有两个:第一个元素不会重复;排序后与前一个元素不同的元素不会重复
a[j++] = a[i];
}
return a.begin() + j; //返回未排好序的部分的第一个位置
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
int x,c;
int l,r;
cin >> n >> m;
while(n--){
cin >> x >> c;
add.push_back({x,c}); //放入二元组vector中,方便之后对位置x进行加法操作
pos.push_back(x); //放入坐标位置
}
while(m--){
cin >> l >> r;
query.push_back({l,r}); //也是放入二元组vector中,方便最后进行查询操作
pos.push_back(l); //放入坐标位置
pos.push_back(r);
}
sort(pos.begin(),pos.end()); //先对存储坐标的数组进行排序
pos.erase(unique(pos.begin(),pos.end()),pos.end()); //然后去重
for(auto i : add){ //进行加法操作
int y = find(i.first); //取出坐标位置,并离散化,之后再做加法
a[y] += i.second; //加法
}
for(int i = 1; i <= pos.size(); ++i) //求前缀和
f[i] = f[i-1] + a[i];
for(auto i : query){ //进行查询操作
l = find(i.first); //取出区间坐标后,离散化,再求对应的前缀和
r = find(i.second);
cout << f[r] - f[l - 1] << endl;
}
}
八、区间合并
1、将每个区间按左端点从小到大进行排序;
2、排好序后的区间间有三种关系,且未合并好的区间都在在当前处理区间的的右侧,如图所示,可分3种情况:
1.下一区间与当前区间不相交;
2.下一区间与当前区间部分相交;
3.下一区间在当前区间内部
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> pii;
vector<pii> segs; //定义二元组,包括区间起点和终点
void merge(vector<pii> &segs)
{
vector<pii> temp; //设置合并后的区间组
sort(segs.begin(),segs.end()); //按照区间起点大小进行排序
int start=-2e9,end=-2e9; //设置区间,为inf
for(auto seg : segs){ //排好序后的区间间有三种关系,且未合并好的区间都在在当前处理区间的的右侧:1.下一区间与当前区间不相交;2.下一区间与当前区间部分相交;3.下一区间在当前区间内部
if(seg.first > end){ //第一种情况,区间不相交
if(start != -2e9) //去除最开始时的inf区间
temp.push_back({start,end}); //若下一区间不与当前区间相交,则之后的区间更加不会与当前区间相交,因为是按区间起点排好序过的,所以当前区间不会再改变了,放入已合并区间中
start = seg.first; //更新当前区间
end = seg.second;
}
else{
end = max(end,seg.second); //第二,三种情况
}
}
if(end != 1e9) //由于之前已合并区间的放入都是当前区间和下一区间进行比较后,若不相交才把当前区间放入已合并区间中,而最后一个当前区间没有其他区间与它进行比较,所以还需要额外把最后一个当前区间放入已合并区间中
temp.push_back({start,end});
segs = temp; //将已合并区间放入答案中
}
int main()
{
int n;
int l,r;
cin>>n;
while(n--){
scanf("%d%d",&l,&r);
segs.push_back({l,r}); //区间起点,终点二元组的放入
}
merge(segs);
cout << segs.size() << endl;
return 0;
}
区间类型题套路写法(如果不用记录具体区间的话res都不用vector,直接int就行):
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10, inf = 0x3f3f3f3f;
struct side{
int l, r;
bool operator < (const side & t) const{
if(l < t.l || (l == t.l && r <= t.r)) return 1;
return 0;
}
}s[N];
vector<side> res;
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; ++ i) scanf("%d%d", &s[i].l, &s[i].r);
sort(s, s + n);
int st = -inf, ed = -inf;
for(int i = 0; i < n; ++ i){
if(s[i].l > ed){
if(st != -inf) res.push_back({st, ed});
st = s[i].l;
ed = s[i].r;
}
else ed = max(ed, s[i].r);
}
if(ed != -inf) res.push_back({st, ed});
cout << res.size() << endl;
return 0;
}