倍增 从字面的上意思看就是成倍的增长 ,这是指我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间和空间复杂度的要求 ,那么我们就可以通过成倍的增长,只递推状态空间中在2的整数次幂位置上的值作为代表 。当需要其他位置上的值时,我们只需要通过" 任意整数可以表示成若干个2的次幂项的和 ",例如 , 使用之前求出的代表值根据题目的性质进行运算求出所需的值。
在ST表中的应用
例如在ST表中我们现在要O(1)求出区间最大值,一个很自然的想法便是记录f(i,j)为[i,j]内的最大值显然有转移方程f(i,j)=max(f(i,j-1), a[j] ) 但是这样预处理是O(N^2)的,不能通过,我们考虑进一步优化观察到一个性质:max(a,b,c)=max(max(a,b),max(b,c)) 就是我们可以由两个较小的、有重叠的区间直接推出一个大区间,因此我们可以少维护一些区间。这里也是这样,我们采用倍增思想,令f(i,j)为从a[i]开始的、连续2^j个数的最大值,显然:f(i,0)=a[i](显然根据定义可得)f(i,j)=max(f(i,j-1),f(i+2^(j-1),j-1)。 这样我们就可以在log(n)的时间里面处理好区间长度为2的幂次的情况,即[l,r] 之间的长度为2的幂次时,例:找[1,4] 的最大值,我们直接给出f[1,2] 的值即可。但要是问[1,6] 的最大值呢? 这时我们就要用到max的性质了,我们求max(f[1,2] ,f[3,2]) 即可。
在LCA的应用
在lca中我们以pre[i][j]代表i节点它往上的第2^j个父亲,与ST表类似,我们可以观察到一个性质:pre[i][j]==pre[pre[i][j-1]][j-1] 即A的父亲是B,B的父亲是C那么pre[A][0]=B,pre[B][0]=C,那么pre[A][1]=pre[pre[A][0]][0]=pre[B][0]=C 同理 pre[A][2]=E
由此我们可以求出i往上任意2的j次方个父亲,那么如果不是2的幂次呢,比如要找i往上的第5个父亲,我们可以从高到底枚举该位,比如5写成2进制是101,那么第2位是1我们另u=pre[u][2],然后第1位是0不变,然后第0位是1,我们令u=pre[u][0],最终u就是所求的父亲。
int lca(int u,int v){
if(depth[u]<depth[v]) swap(u,v); //保证u的深度>=v的深度
for(int i=20;i>=0;i--){
if(depth[pre[u][i]]>=depth[v]){
u=pre[u][i];
}
if(u==v){ //到达相同深度时两个节点相遇
return u;
}
}
for(int i=20;i>=0;i--){ //到达相同深度时两个节点没有相遇
if(pre[u][i]!=pre[v][i]){
u=pre[u][i];
v=pre[v][i];
}
}
return pre[u][0];
}
考虑令u和v往上找父亲直到他们深度相同
参考以下代码,如果depth[u]=10,depth[v]=3,那么过程如下
i=3,depth[u][i]=2<depth[v] continue;
i=2 depth[u][i]=6>depth[v] u=depth[u][2]
i=1 depth[u][i]=4>depth[v] u=depth[u][1]
i=0 depth[u][i]=3==depth[v] u=depth[u][0]
break;
考虑u和v当前深度相同找到他们的lca
- 如果u和v目前是同一个节点直接return u
- 否则,从大到小枚举2^i个父亲,如果pre[u][i]==pre[v][i],有可能找到了父亲
但也有可能找过头了
所以保险起见,我们不断让u,v往上找父亲,但是确保pre[u][i]!=pre[v][i],这样到最后的u,v一定是离他们父亲距离最近的节点,其实就是他们父亲的儿子节点,最后return pre[u][0]即可
此外在LCA中pre[i][j]根据题目还可以表示别的信息,如下题
https://ac.nowcoder.com/acm/problem/13331
这道题与普通的lca不同的有两点:
- u,v一定在同一条链上
- u->v的路径上经过的点的价值是单调递增的
根据1和2的条件,我们可以确定pre[u][i]所对应的父亲不再是往上的第2^i个点,而相当于是u往上第2^i个单调递增的点,当知道每个点的第一个父亲,即pre[u][0]后,根据pre[u][i]=pre[pre[u][i-1]][i-1]别的都可以递推出来。
对于求每个点的第一个父亲,即求每个点往上第一个比它大的点是谁,如果用暴力是O(n^2)的肯定会超时,解决方案:我们考虑在dfs的过程中如果目前到了点u,u上面的点是v,那么可以确定v到root所有点的pre[i][0]都已经确定了,那么v-root所有点的任意幂次pre[i][j]也都已经确定了,即任取x,pre[v][x]都已经确定了,与上文的u,v往上找父亲直到深度类似。
- 从大到小枚举v往上的第2^i个父亲
- 当它存在并且小于等于val[u]时说明2^i个父亲还不够大,往上跳,v=pre[v][i];
否则说明val[v]>val[u],有可能是父亲也有可能是父亲的父亲的父亲...,找的太过了,continue;
最后结束的v即为最后一个val[v]小于等于val[u]的节点,它的父亲的val就会大于val[u],与上文的u,v深度相同找父亲类似,代码如下
int x=v;
for(int i=20;i>=0;i--){
if(pre[v][i]&&a[u]>=a[pre[v][i]]){
v=pre[v][i];
}
}
pre[u][0]=pre[v][0];
倍增的综合应用
例题1:
令dp[i][j][k]代表从i走到j正好经过2^k条路径的最短距离:dp[i][j][0]即为初始的i-j的边的距离,如果没有则为无穷大
状态转移方程:
dp[i][j][k]=min(dp[i][j][k],dp[i][p][k-1]+dp[p][j][k-1]) 例如从i->j经过4条路径的最短距离就等于
min(从i->p经过2条路径的最短距离+从p->j经过2条路径的最短距离)
证明:例如到了算4条路径的时候,任意两点经过2条路径的最短距离都已经算出来过了,并且i->j经过4条路径一定是有点中间点p存在满足从i->p经过两条路径,从p->j经过两条路径的,所以枚举p即可得出答案
但是如果i->j经过的路径条数n不是2的幂次呢?
假设n=11 那么二进制即为2^3+2^1+2^0,其实那么我们只要用2^3,2^1,2^0这些幂次的dp[i][j][k]来更新即可,比如i->j经过11条边的最短距离就等于min(i->p经过10条边的最短距离+p->j经过1条边的最短距离),而i->j经过10条边的最短距离可以看作min(i->p经过8条边的最短距离加上p->j经过2条边的最短距离)
#include<bits/stdc++.h>
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=200+5;
const int inf=1e9+7;
map<int,int>mp;
int dis[maxn][maxn];
int dp[maxn][maxn][maxn];
int tmp[maxn][maxn];
int n;
void cal(int mici){
memset(tmp,0x3f,sizeof(tmp));
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
tmp[i][j]=min(tmp[i][j],dis[i][k]+dp[k][j][mici]);
}
}
}
memcpy(dis,tmp,sizeof(tmp));
}
void solve(){
memset(dis,0x3f,sizeof(dis));
memset(dp,0x3f,sizeof(dp));
int num,t,s,e;
cin>>num>>t>>s>>e;
int cnt=0;
for(int i=1;i<=t;i++){
int u,v,w;
cin>>w>>u>>v;
if(!mp.count(u)){
cnt++;
mp[u]=cnt;
}
if(!mp.count(v)){
cnt++;
mp[v]=cnt;
}
dis[mp[u]][mp[v]]=min(dis[mp[u]][mp[v]],w);
dis[mp[v]][mp[u]]=min(dis[mp[u]][mp[v]],w);
}
n=cnt;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j][0]=dis[i][j];
}
}
for(int k=1;k<=20;k++){
for(int p=1;p<=n;p++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j][k]=min(dp[i][j][k],dp[i][p][k-1]+dp[p][j][k-1]);
}
}
}
}
int ans=0;
num--; //自己本身对应的就是dp[i][j][0],所以减一
for(int i=20;i>=0;i--){ //枚举幂次
if((num>>i)&1){
cal(i);
}
}
cout<<dis[mp[s]][mp[e]]<<"\n";
}
int main(){
int t=1;
//cin>>t;
while(t--){
solve();
}
}
例题2:
题意:
由无限数量的土豆,前 个土豆的重量
被给出,第i个土豆的重量为
现在需要把这些土豆按照这样的操作依次放进盒子里:
从第一个土豆开始,按顺序放在第一个盒子里,直到放完后,如果当前盒子中土豆的总重量大于等于 时,将该盒子封装好。拿第二个盒子开始装下一个土豆,直到重量大于等于
,把盒子封装起来。无限重复此操作。
问:第 个盒子装了多少个土豆?
思路:
假设当前盒子为空,目前要从重量为的土豆开始装,则装满当前盒子时,需要连续装num[i]个土豆,下一个盒子从
开始装,那么
,即可倍增解决
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
int x;
cin >> x;
vector<int> w(n);
for (int i = 0; i < n; i++) {
cin >> w[i];
}
vector<int> sum(n + 1);
for (int i = 0; i < n; i++) {
sum[i + 1] = sum[i] + w[i];
}
vector<int> num(n);
const int lg = 40;
vector f(lg + 1, vector<int>(n));
int xm = x % sum[n];
for (int i = 0, j = 0; i < n; i++) {
while (sum[n] * (j / n) + sum[j % n] < sum[i] + xm) {
j++;
}
num[i] = j - i + x / sum[n] * n;
f[0][i] = j % n;
}
for (int j = 0; j < lg; j++) {
for (int i = 0; i < n; i++) {
f[j + 1][i] = f[j][f[j][i]];
}
}
for (int i = 0; i < q; i++) {
int k;
cin >> k;
k--;
int c = 0;
for (int j = lg - 1; j >= 0; j--) {
if (k >> j & 1) {
c = f[j][c];
}
}
cout << num[c] << "\n";
}
return 0;
}