倍增思想的应用

倍增 从字面的上意思看就是成倍的增长 ,这是指我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间和空间复杂度的要求 ,那么我们就可以通过成倍的增长,只递推状态空间中在2的整数次幂位置上的值作为代表 。当需要其他位置上的值时,我们只需要通过" 任意整数可以表示成若干个2的次幂项的和 ",例如13=2^{3}+2^{2}+2^{0} , 使用之前求出的代表值根据题目的性质进行运算求出所需的值。

在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不同的有两点:

  1. u,v一定在同一条链上
  2. 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:

题意:

由无限数量的土豆,前 N(n\leq 2\ast 10^{5})个土豆的重量 W(W_{1},W_{2},...W_{N})被给出,第i个土豆的重量为W_{(i-1)mod N+1}

现在需要把这些土豆按照这样的操作依次放进盒子里:

从第一个土豆开始,按顺序放在第一个盒子里,直到放完后,如果当前盒子中土豆的总重量大于等于 X时,将该盒子封装好。拿第二个盒子开始装下一个土豆,直到重量大于等于X ,把盒子封装起来。无限重复此操作。

问:第 K_{i}个盒子装了多少个土豆?

思路:

假设当前盒子为空,目前要从重量为W_{i}的土豆开始装,则装满当前盒子时,需要连续装num[i]个土豆,下一个盒子从W_{f[i][0]}开始装,那么f[i][j]=f[f[i][j-1]][j-1],即可倍增解决

#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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值