算法基础Part.3
前缀和与差分
建议数组以1开始,这样方便
例. AcWing99 激光炸弹
地图上有 $N$ 个目标,用整数 $X_{i}, Y_{i}$ 表示目标在地图上的位置,每个目标都有一个价值 $W_i$。
注意:不同目标可能在同一位置。
现在有一种新型的激光炸弹,可以摧毁一个包含 $R \times R$ 个位置的正方形内的所有目标。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 $x,y$ 轴平行。
求一颗炸弹最多能炸掉地图上总价值为多少的目标。
输入格式
第一行输入正整数 $N$ 和 $R$,分别代表地图上的目标数目和正方形的边长,数据用空格隔开。
接下来 $N$ 行,每行输入一组数据,每组数据包括三个整数 $X_{i}, Y_{i}, W_{i}$,分别代表目标的 $x$ 坐标,$y$ 坐标和价值,数据用空格隔开。
输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。
数据范围
$0 \le R \le 10^9$
$0 < N \le 10000$,
$0 \le X_{i}, Y_{i} \le 5000$
$0 \le W_i \le 1000$
输入样例:
2 1
0 0 1
1 1 1
输出样例:
1
题解
#include <iostream>
using namespace std;
const int N = 5010;
int g[N][N];
int main()
{
int N, r;
cin >> N >> r;
int n = r, m = r;
for (int i = 0, x, y, w; i < N; i++)
{
cin >> x >> y >> w;
x++, y++;//写前缀和时坐标从1开始可以不用处理边界问题
n = max(n, x); m = max(m, y);//得到区域的下边界和右边界
g[x][y] += w;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
g[i][j] = g[i][j] + g[i - 1][j] + g[i][j - 1] - g[i - 1][j - 1];//动态规划的思想,让g直接变成前缀和数组
}//这一行的具体实现是在大脑中想想一张图,把每个点的w变为每个点左上方w总和
}
int res = 0;
for (int i = r; i <= n; i++)
{
for (int j = r; j <= m; j++)
{
res = max(res, g[i][j] - g[i - r][j] - g[i][j - r] + g[i - r][j - r]);
}//再根据二维前缀和暴力计算题目所要求的R*R最大值,!多用max和min,方便
}
cout << res << endl;
return 0;
}
例. AcWing100 增减序列
给定一个长度为 $n$ 的数列 ${a_1,a_2,…,a_n}$,每次可以选择一个区间 $[l,r]$,使下标在这个区间内的数都加一或者都减一。
求至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列可能有多少种。
输入格式
第一行输入正整数 $n$。
接下来 $n$ 行,每行输入一个整数,第 $i+1$ 行的整数代表 $a_i$。
输出格式
第一行输出最少操作次数。
第二行输出最终能得到多少种结果。
数据范围
$0 < n \le 10^5$,
$0 \le a_i <2147483648$
输入样例
4
1
1
2
2
输出样例
1
2
题解
//这道题用差分,因为我们知道,一个数列,把他每一项视为某个数列的前n项和,即那个数列为差分数列的时候,对差分数列某个数+1,就会让原数列对应数字及之后所有数字都+1,因为+1累积于那个+1,这个时候,通过差分序列有多少正数,有多少负数,我们可以知道它们相对于第一个数字,有多少偏差,每一个正数可以抵消一个等绝对值的负数,所以首先必须要进行min(正数总和,绝对值的负数的总和),然后根据abs(两种数绝对值之差),算出要进行多少次[1,x]或[x,n],的操作,因为对差分数列某个数操作,会让或某个数列之后所有数全部发生相同变化,而某两个数,一加一减,就是l,r那一段全部相同变化,而最少操作总数则取决于abs(两种数绝对值之差)+1是多少,也就是左端或右端整个一段平衡的分配,比如abs=5,则有左端全体操作5次,右端0;1,4;2,3;3,2;4,1;5,0;这一共六种操作。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int a[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = n; i > 1; i--) a[i] = a[i] - a[i - 1];
LL pos = 0, neg = 0;
for (int i = 2; i <= n; i++)
{
if (a[i] > 0) pos += a[i];
else neg = neg - a[i];
}
cout << min(pos, neg) + abs(pos - neg) << endl;
cout << abs(pos - neg) + 1 << endl;
return 0;
}
例. AcWing101 最高的牛
有 $N$ 头牛站成一行,被编队为 $1、2、3…N$,每头牛的身高都为整数。
当且仅当两头牛中间的牛身高都比它们矮时,两头牛方可看到对方。
现在,我们只知道其中最高的牛是第 $P$ 头,它的身高是 $H$ ,剩余牛的身高未知。
但是,我们还知道这群牛之中存在着 $M$ 对关系,每对关系都指明了某两头牛 $A$ 和 $B$ 可以相互看见。
求每头牛的身高的最大可能值是多少。
输入格式
第一行输入整数 $N, P, H, M$,数据用空格隔开。
接下来 $M$ 行,每行输出两个整数 $A$ 和 $B$ ,代表牛 $A$ 和牛 $B$ 可以相互看见,数据用空格隔开。
输出格式
一共输出 $N$ 行数据,每行输出一个整数。
第 $i$ 行输出的整数代表第 $i$ 头牛可能的最大身高。
数据范围
$1 \le N \le 10000$,
$1 \le H \le 1000000$,
$1 \le A,B \le 10000$,
$0 \le M \le 10000$
输入样例
9 3 5 5
1 3
5 3
4 3
3 7
9 8
输出样例
5
4
5
3
4
4
5
5
5
注意:
- 此题中给出的关系对可能存在重复
题解
#include <iostream>
#include <set>
using namespace std;
const int N = 10010;
int height[N];
int main()
{
int n, p, h, m;
cin >> n >> p >> h >> m;
height[1] = h;//这个时候height还是每头牛高度的差分序列,也就是说只要height[1]=h,就相当于所有牛初始值都为1
//此题有个特性就是所有要求区间不会交叉,因为交叉会有悖论,物理上不可能,无法做到。假设一下就明白
//所以当区间是嵌套/平行关系的时候,只要先让所有牛是最大值,然后再根据要求让夹在中间的牛身高都减1即可
set<pair<int, int>> existed;
for (int i = 0, a, b; i < m; i++)
{
cin >> a >> b;
if (a > b) swap(a, b);
if (!existed.count({ a,b }))
{
existed.insert({ a,b });
height[a+1]--, height[b]++;//原本差分是a~b+1,然后根据这题实际意义是这两端中间都减1,就是a-1~b,即保证两端都能看见
}
}
for (int i = 1; i <= n; i++)
{
height[i] = height[i - 1] + height[i];//将差分序列变为原本真正表示身高的数列,并输出。
cout << height[i] << endl;
}
return 0;
}//差分特点是擅长对一个数列的一部分/一个区间进行整体操作,最后再反映到原数据/数列上。
注
ios::sync_with_stdio(false);加快cin,但副作用是有的函数无法使用。
关键是Sn-Sn-1=an,Sn=Sn-1+an的使用,以及al+=c ar-=c原数列对整体的影响
双指针算法
核心:将暴力的i、j枚举n2复杂度变为小于n2,比如快排、归并的双指针
步骤
//步骤:
//1、先写出朴素做法
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
if(check(i,j))
do操作
}
}
//2、根据指针性质改写:双指针算法
for(int i=0,j=0;i<n;i++)//注意,j走过的路,就不在走了,j=0只被赋值了一次,j不会回头
{
do sth
while(do sth) do sth j++;//具体题目具体分析
do操作;
}
//大幅降低时间复杂度
例.AcWing799 最长连续不重复子序列
给定一个长度为 $n$ 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 $n$。
第二行包含 $n$ 个整数(均在 $0 \sim 10^5$ 范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
$1 \le n \le 10^5$
输入样例
5
1 2 2 3 5
输出样例
3
题解
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int a[N],s[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int res = 0;
for (int i = 0,j = 0; i < n; i++)
{
s[a[i]]++;
while (s[a[i]]>1)
{
s[a[j]]--;
j++;//不断右移j直到区间内重复的一个数字被排除
}
res = max(res, i - j + 1);
}
printf("%d", res);
return 0;
}
位运算
lowbit函数
具体写法:
lowbit(x)=x&(-x)=x&(~x+1)波浪号为取反。
作用:
返回x的最后一位1以及之后的0,以二进制形式。
比如lowbit(1010101000)=1000 (这里的数字都是二进制)
简单应用:计算一个数1在哪些位,不断给原数减去lowbit可以遍历每一个1(二进制下)
离散化:将大范围离散数字映射到自然数上,先去重,以下标为映射值,找数字对应的下标就利用二分:值域大数据却稀疏
模板
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
//unique是将所有重复元素放到数组后方,然后返回第一个重复元素位置的函数
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于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; // 映射到1, 2, ...n,若不+1就是从0开始的数组下标
}
//如果大范围数字本身就对应什么小范围数字,对大范围数字用find函数,把对应的数字存到一个新数组里。
例. AcWing802 区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 $0$。
现在,我们首先进行 $n$ 次操作,每次操作将某一位置 $x$ 上的数加 $c$。
接下来,进行 $m$ 次询问,每个询问包含两个整数 $l$ 和 $r$,你需要求出在区间 $[l, r]$ 之间的所有数的和。
输入格式
第一行包含两个整数 $n$ 和 $m$。
接下来 $n$ 行,每行包含两个整数 $x$ 和 $c$。
再接下来 $m$ 行,每行包含两个整数 $l$ 和 $r$。
输出格式
共 $m$ 行,每行输出一个询问中所求的区间内数字和。
数据范围
$-10^9 \le x \le 10^9$,
$1 \le n,m \le 10^5$,
$-10^9 \le l \le r \le 10^9$,
$-10000 \le c \le 10000$
输入样例
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例
8
0
5
题解
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N = 300010;
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;//此题用前缀和所以最小从1开始
}
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.begin(),alls.end()),alls.end());
for(auto item : add)
{
int x = find(item.first);
a[x]+=item.second;
}
for(int i = 1;i<=alls.size();i++) s[i]=a[i]+s[i-1];
for(auto item : query)
{
int l=find(item.first),r=find(item.second);
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
启发
离散化的这种映射为自然数的操作是彻底的,也是双向的,完全用find()和自然数替代了原本的大数字与大数字对应,不仅是将大数字与自然数建立关系,也让大数字对应的(也可以说是键值对的值)数与自然数建立关系(通过find()这套规则以及将题目所给的数据重新再存入一个新的vector),之后便可依靠find完全抛弃大数字,转而直接对新的数组之间操作。还是多做题。
区间和并:以左或右端点排序
1、按照区间左端点排序
2、从左到右扫描区间,选取最左侧区间为标准,与其他区间只有三种情况 a包含 b相交(左端包含) c在右侧
3、遇到c更新区间
详见例题
例.AcWing803 区间和并
给定 $n$ 个区间 $[l_i, r_i]$,要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:$[1,3]$ 和 $[2,6]$ 可以合并为一个区间 $[1,6]$。
输入格式
第一行包含整数 $n$。
接下来 $n$ 行,每行包含两个整数 $l$ 和 $r$。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
$1 \le n \le 100000$,
$-10^9 \le l_i \le r_i \le 10^9$
输入样例
5
1 2
2 4
5 6
7 8
7 9
输出样例
3
题解
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N = 100010;
PII a[N];
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
int l,r;
cin>>l>>r;
a[i]={l,r};
}
sort(a,a+n);
int res=0,max_r=-2e9;
for(int i=0;i<n;i++)
{
if(max_r<a[i].first)
{
res++;
max_r=a[i].second;
}
else max_r=max(max_r,a[i].second);
}
cout<<res;
return 0;
}

浙公网安备 33010602011771号