[基本算法]倍增

特点与二分对比

  • 和二分类似,也是加速枚举过程。

  • 不同之处:倍增通常需要预处理一些东西,预处理复杂度高,判断合法性复杂度低。二分则相反。

ST表

  • ST表是一种很好的反应倍增思想的数据结构,不仅限于维护区间内的最大值,下面例题(坑了半天的紫题)可以很好的体现出这一点。

用一个 f[ i ][ j ] 二位数组来维护区间[ i, i + 2^j - 1 ]的最大值,其预处理方法与树上 LCA 类似,不再赘述。

释其对于某个区间的查询功能还是有必要的:

计算出log2 |S|,S=r - l + 1,然后对于左端点和右端点分别进行查询:

int Query(int l,int r)
{
    int c=log2(r-l+1); 
    return max(st[l][c],st[r-(1<<c)+1][c]);//把拆出来的区间分别取最值 
}

代码如下:类似于区间 dp

#include <iostream>
#include <cstdio>
#include <cmath>
#define re register
using namespace std;
const int maxn=1e6 + 10;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int f[maxn][21];
int n,m;
inline int query(int l,int r){
	int c=log2(r-l+1);
	return max(f[l][c],f[r-(1<<c)+1][c]);
}
int main(){
	//freopen("1.in","r",stdin);
	n=read();m=read();
	for(re int i=1;i<=n;i++)f[i][0]=read();//他自己 
	for(re int j=1;j<=20;j++)//状态 
		for(re int i=1;i+(1<<j)-1<=n;i++){
			f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);
		}
	for(re int i=1;i<=m;i++){
		int l=read(),r=read();
		printf("%d\n",query(l,r));
	}	
	return 0;
}

例题 NOI2010超级钢琴

传送门

题目大意

给你一个长度为n的序列a1,a2,a3...an,找出 k个子序列使得这 k 个子序列的和最大,且这 k 个子序列的长度在l 到 r之间。

题目分析

这是一道非常经典的 st 表优化的题,闭上眼睛想一想:为什么用 st 表??怎么优化??

  • 要求最大子序列,显然要用到前缀和,这是一个思考方向。

  • 为什么用 st 表?因为出现了负数。前缀和肯定不是一直增大的,我们如果单纯暴力算出最大字段和会造成极大的时间浪费,借助st表的功能,因此st 表维护的是前缀和的最大值。

下面怎么求解??

采用问题简化的思想(做题即翻译,先往最简单的方面想):

我们固定一个起点i,怎么求以它为开头的最大字段和??

显然是找到一个k点,使得sum[k]-sum[i-1]最大化,这不就是st表维护和查询的功能的典型体现吗?但是st表查询是要有区间限制的,否则这题也没意义(想一想,为什么?)。不难看出,k的取值范围是,[i+l-1,i+r-1],但是这里有一个细节,i+r-1要和n取最小值。

有了思路,怎么实现?

  • 依据上面的思路,我们需要用st表查询最大前缀和,从而求出最大字段和,因此初始化st表就简单了
for(re int i=1;i<=n;i++)f[i][0]=sum[i];
for(re int j=1;j<=19;j++)
	for(re int i=1;i<=n;i++){
		d[i][j]=d[d[i][j-1]+1][j-1];
		f[i][j]=max(f[i][j-1],f[d[i][j-1]+1][j-1]);
	}

能有这么简单?我很快就到你家门口

我们把此时的答案记作f(id,l,r),表示以 id 为起点的到l到r区间某个点 k 的最大字段和,满足了题意求一个的目的。这个普通的RMQ有什么区别呢?在于我们需要知道我们取最大值的位置,不然我们会不知道我们取了哪一串音符,有可能会取重或者取不到(这是一个理由)。

  • 因此记录最大值出现的位置
for(re int j=1;j<=19;j++)
		for(re int i=1;i<=n;i++){
			d[i][j]=d[d[i][j-1]+1][j-1];//类似于LCA倍增 
			if(f[i][j-1]>f[d[i][j-1]+1][j-1]){
				f[i][j]=f[i][j-1];
				id[i][j]=id[i][j-1];
			}
			else{
				f[i][j]=f[d[i][j-1]+1][j-1];
				id[i][j]=id[d[i][j-1]+1][j-1];
			}
		}
        
  • 查询操作
inline int query(int L,int R){//??? 
	int maxx=-INF,pos;
	for(re int j=19;j>=0;j--){
		if(L+(1<<j)-1<=R){
			if(f[L][j]>maxx)maxx=f[L][j],pos=id[L][j];	
			L=d[L][j]+1;
		}
	}
	return pos;
}

更重要的是下面的,会求一个,多个怎么求??需要用到大根堆处理这个问题,用优先队列实现,以sum[ k ] - sum[ id-1 ]的值重载小于号,表示f(id,l,r)的值。

首先我们可以将 n 个四元组f(id,i,j,nw)加入优先队列中,此时我们先取出一个队顶元素,这肯定是全局最大的值了,如何找到第二大的呢?

  • 我们可以先破开最大的这一个,将其分成两段:需要特判

1.(id,l,nw-1,f(id,l,nw-1)取到的最大值 k)

2.(id ,nw+1,r,f(id,nw+1,r)取到的最大值 k)

取出 m 次后,ans更新,得到答案。

#include <iostream>
#include <cstdio>
#include <queue>
#define re register
using namespace std;
const int maxn=5e5 + 10,INF=2e9;
int n,k,l,r,sum[maxn],f[maxn][20],id[maxn][20],d[maxn][20];//d是辅助空间 
long long ans;
struct node{
	int id,l,r,nw;
	bool operator <(const node &x)const{
		return sum[nw]-sum[id-1]<sum[x.nw]-sum[x.id-1];
	}
};
priority_queue <node> q;
inline int query(int L,int R){//??? 
	int maxx=-INF,pos;
	for(re int j=19;j>=0;j--){
		if(L+(1<<j)-1<=R){
			if(f[L][j]>maxx)maxx=f[L][j],pos=id[L][j];	
			L=d[L][j]+1;
		}
	}
	return pos;
}
int main(){
	//freopen("1.in","r",stdin);
	scanf("%d%d%d%d",&n,&k,&l,&r);
	for(re int i=1;i<=n;i++){
		int tmp;scanf("%d",&tmp);
		sum[i]=sum[i-1]+tmp;
		d[i][0]=id[i][0]=i;
		f[i][0]=sum[i];//针对谁建st表谁就是初始化值 
	}
	for(re int j=1;j<=19;j++)
		for(re int i=1;i<=n;i++){
			d[i][j]=d[d[i][j-1]+1][j-1];//类似于LCA倍增 
			if(f[i][j-1]>f[d[i][j-1]+1][j-1]){
				f[i][j]=f[i][j-1];
				id[i][j]=id[i][j-1];
			}
			else{
				f[i][j]=f[d[i][j-1]+1][j-1];
				id[i][j]=id[d[i][j-1]+1][j-1];
			}
		}
	for(re int i=1;i<=n-l+1;i++){//注意细节 
		node tmp;
		tmp.id=i; 
		tmp.nw=query(i+l-1,min(n,i+r-1));//又一个细节
		tmp.l=i+l-1;tmp.r=min(n,i+r-1);
		q.push(tmp); 
	}
	while(k--){//拆! 
		node t=q.top();q.pop();node tmp;
		ans+=sum[t.nw]-sum[t.id-1];
		if(t.nw>t.l){//左 
			tmp.l=t.l;tmp.r=t.nw-1;
			tmp.id=t.id;//起点是不变的 
			tmp.nw=query(tmp.l,tmp.r);
			q.push(tmp);
		}
		if(t.nw<t.r){
			tmp.l=t.nw+1;tmp.r=t.r;
			tmp.id=t.id;
			tmp.nw=query(tmp.l,tmp.r);
			q.push(tmp);
		}
	}
	printf("%lld",ans);
	return 0;
}

例题,树上倍增

传送门

题目大意(做题即翻译)

  • 给出你 n 个点的无向图,以及 m 条已确定的带有权值的边。

  • 对于任意的一个三元组( i , j , k),如果 dis[ i ][ j ]< dis[ i ][ k ] and dis[ i ][ j ] < dis[ j ][ k ],就不好。

  • 让你更改其余所有边的权值,使得这个图好起来,并且权值和最小(应该是个完全图,任意两点间都有连边)

分析

容易得到,如果对于任意的三元组已经确定了两条边的权值为a,b,那么第三条边的权值必为 min ( a , b )。(这是从题目最本质的点出发)

继续把上面结论推广,如果在图中存在一条从 u , 到 v 的路径,那么 dis[ u ][ v ] 必为路径上的最小权值,否则就会出现题目中的无解状态,输出 -1 即可。

我们来看原题的样例二:很明显这就是个无解状态。

所以怎么来求最小权值和呢?

先考虑已经给出的 m 条边,他们的权值都是确定的。

再说剩下的,我们称作不在同一个连通分量里的边,直接连权值 1 的边是没有任何问题的(可以举反例)。那么所有的边就考虑完了,但是基于要统计所有的 min ,我们不可能枚举每两个点来统计,会 T 飞。

闭上眼睛想一想:这无妨就是一个求最大生成树的过程

  • 那些还无法确定的边(在同一个连通分量里,不要和上面的连 1 搞混)等于这棵树上两个点路径上最小的那条边最优

  • 而对于那些没有加入这棵生成树中的确定边,必须等于这条路径上的最小边,否则非法直接-1。换句话说,考虑一条边(u ,v , w ),u 到 v 的所有路径中的最小值必定等于w,否则输出 -1,无解。

倍增思想就运用在了维护最小值上

//from kupi
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
int const maxn = 300003, inf = 0x3f3f3f3f;

struct Edge {
	int x, y, z;
};
inline bool operator<(Edge const &lhs, Edge const &rhs) { return lhs.z > rhs.z; }

int n = 0, m = 0;
Edge a[maxn];

int head[maxn], nxt[maxn << 1], to[maxn << 1], val[maxn << 1], cnt = 0;
inline void insert(int u, int e, int v) {
	nxt[++cnt] = head[u];
	head[u] = cnt;
	to[cnt] = e;
	val[cnt] = v;
}

namespace calc {
	int fa[maxn], siz[maxn], mi[maxn];
	inline int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }

	ll Kruskal() {
		sort(a + 1, a + m + 1);
		for (int i = 1; i <= n; ++i) {
			fa[i] = i;
			siz[i] = 1;
			mi[i] = inf;
		}
		ll counted = 0, sum = 0;
		for (int i = 1; i <= m; ++i) {
			int x = find(a[i].x), y = find(a[i].y);
			if (x != y) {
				sum += ll(siz[x]) * siz[y] * a[i].z;
				counted += ll(siz[x]) * siz[y];
				if (siz[x] < siz[y]) std::swap(x, y);
				fa[y] = x;
				siz[x] += siz[y];
				mi[x] = a[i].z;
				insert(a[i].x, a[i].y, a[i].z);
				insert(a[i].y, a[i].x, a[i].z);
			} else if (mi[x] > a[i].z)
				return -1;
		}
		sum += (ll(n) * (n - 1ll) / 2 - counted);
		return sum;
	}
} // namespace calc

namespace check {
	bool vis[maxn];
	int dep[maxn], fa[19][maxn], mi[19][maxn];
	void dfs(int x) {
		vis[x] = true;
		for (int j = 1; j <= 18; ++j) {
			fa[j][x] = fa[j - 1][fa[j - 1][x]];
			mi[j][x] = min(mi[j - 1][x], mi[j - 1][fa[j - 1][x]]);
		}
		for (int i = head[x]; i; i = nxt[i])
			if (!vis[to[i]]) {
				dep[to[i]] = dep[x] + 1;
				fa[0][to[i]] = x;
				mi[0][to[i]] = val[i];
				dfs(to[i]);
			}
	}
	int query_min(int x, int y) {
		if (dep[x] < dep[y]) swap(x, y);
		int ans = inf;
		for (int j = 18; ~j; --j)
			if (dep[fa[j][x]] >= dep[y]) {
				ans = min(ans, mi[j][x]);
				x = fa[j][x];
			}
		if (x == y) return ans;
		for (int j = 18; ~j; --j)
			if (fa[j][x] != fa[j][y]) {
				ans = min(ans, min(mi[j][x], mi[j][y]));
				x = fa[j][x];
				y = fa[j][y];
			}
		ans = min(ans, min(mi[0][x], mi[0][y]));
		return ans;
	}
	bool check() {
		for (int i = 1; i <= n; ++i)
			if (!vis[i]) {
				fa[0][i] = i;
				mi[0][i] = inf;
				dep[i] = 1;
				dfs(i);
			}
		for (int i = 1; i <= m; ++i) {
			int v = query_min(a[i].x, a[i].y);
			if (v != a[i].z) return false;
		}
		return true;
	}
}

int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= m; ++i)
		scanf("%d %d %d", &a[i].x, &a[i].y, &a[i].z);
	ll ans = calc::Kruskal();
	if (ans == -1 || !check::check())
		printf("-1");
	else
		printf("%lld", ans);
	return 0;
}

例题 NOIP2012 开车旅行

传送门

题目大意(做题即翻译)

给你一个已知的有向图,每个结点有一个编号即高度,从西到东排列且只能从西到东走,任意两点间的距离为他俩点的高度差的绝对值。

现在有两个人要轮流开车,任选一个起点 S 出发,A 开一天车,B 开一天车。不同的是,A 都会开往第二近的地方,B 都会开往最近的地方。

问你两个问题:给出你最大行驶距离 x0 ,从哪个点开始走 la / lb 最小(la,lb是他俩开车分别走的距离),第二个问题和第一个类似,求出 la ,lb 即可。

分析

不难发现,处理出每个点的最近城市和次近城市是有必要的,可以用平衡树来维护:

  • 一个点的最近城市无非就是他在平衡树里的前驱或后继

  • 次近城市有两种情况:

  • 1.最近城市是前驱结点,那么次近城市就是前驱的前驱和后继之一。

  • 2.最近城市是后继结点,那么次近城市就是前驱和后继的后继之一。

这里有两个小细节:

1. 由于只能从小往大开,因此要从 n 到 1倒序遍历更新。

2. 在处理 n 的时候可能越界,需要插入两个最大值和两个最小值:h[ 0 ]=INF , h[ n + 1 ] = -INF(想一想,为什么)

因此维护功能就写出来了

h[0]=INF,h[n+1]=-INF;
	node st;
	st.id=0,st.al=INF;
	q.insert(st),q.insert(st);
	st.id=n+1,st.al=-INF;
	q.insert(st),q.insert(st);
	for(int i=n;i;i--){
		int ga,gb;
		node now;
		now.id=i,now.al=h[i];
		q.insert(now);
		set<node>::iterator p=q.lower_bound(now);
		p--;
		int lt=p->id,lh=p->al;//前驱 
		p++,p++;
		int nt=p->id,nh=p->al;//后继
		p--;//回到初始位置
		if(abs(lh-h[i])<=abs(nh-h[i])){//前驱最近 
			gb=lt;
			p--,p--;
			if(abs(p->al-h[i])<=abs(nh-h[i])){//前驱的前驱 
				ga=p->id;
			}
			else ga=nt;
		}
		else{//后继最近 
			gb=nt;
			p++,p++;
			if(abs(p->al-h[i])>=abs(lh-h[i])){
				ga=lt;
			}
			else ga=p->id;
		}
   	}

这里用到了set维护最大和次大值的思想,非常重要。

回到问题上,得到了每个点的最近和次近结点,肯定是要记录的,偷偷看一眼标签,是倍增!因此需要用一个 f(i , j , k)k 先开车存储从 i 号结点走 2^j 次到达的城市结点,得到如下的初始化。同理,采用倍增思想,da db分别表示 A 和 B 的开车距离

	f[i][0][0]=ga,f[i][0][1]=gb;
	da[i][0][0]=abs(h[ga]-h[i]);
	db[i][0][1]=abs(h[gb]-h[i]);

怎么转移?又一个细节,i = 1时,前半段开车和后半段开车的不是一个人:

if(j==1){
	f[i][1][k]=f[i][0][k]+f[f[i][0][k]][0][1-k];
	da[i][1][k]=da[i][0][k]+da[f[i][0][k]][0][1-k];
	db[i][1][k]=db[i][0][k]+db[f[i][0][k]][0][1-k];
}
else if(j>1){
	f[i][j][k]=f[i][j-1][k]+f[f[i][j-1][k]][j-1][k];
	da[i][j][k]=da[i][j-1][k]+da[f[i][j-1][k]][j-1][k];
	db[i][j][k]=db[i][j-1][k]+db[f[i][j-1][k]][j-1][k];
}

因此问题解决了一大半。

怎么模拟行进过程?与倍增的查询是类似的。

void work1(int S,int X){
	int p=S;
	la=0;lb=0;
	for(int i=18;i>=0;i--){//a先开车 
		if(f[p][i][0]&&la+lb+da[p][i][0]+db[p][i][0]<=X){//不能越界 
			la+=da[p][i][0];
			lb+=db[p][i][0];
			p=f[p][i][0];
		}
	}
}

问题一和二的本质是一样的,不知道你发现了没有?

完整代码:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <cstdlib>
using namespace std;
const int maxn=1e5+200,INF=2e9;
struct node{
	int id,al;
	bool operator <(const node &x)const{
		return al<x.al;//按海拔来 
	}
};
int n,m,x0,la,lb;
int h[maxn],s[maxn],x[maxn];
int f[maxn][20][3],da[maxn][20][3],db[maxn][20][3];
double ans=INF*1.0;//控制精度
multiset <node> q;
void prework(){
	h[0]=INF,h[n+1]=-INF;
	node st;
	st.id=0,st.al=INF;
	q.insert(st),q.insert(st);
	st.id=n+1,st.al=-INF;
	q.insert(st),q.insert(st);
	for(int i=n;i;i--){
		int ga,gb;
		node now;
		now.id=i,now.al=h[i];
		q.insert(now);
		set<node>::iterator p=q.lower_bound(now);
		p--;
		int lt=p->id,lh=p->al;//前驱 
		p++,p++;
		int nt=p->id,nh=p->al;//后继
		p--;//回到初始位置
		if(abs(lh-h[i])<=abs(nh-h[i])){//前驱最近 
			gb=lt;
			p--,p--;
			if(abs(p->al-h[i])<=abs(nh-h[i])){//前驱的前驱 
				ga=p->id;
			}
			else ga=nt;
		}
		else{//后继最近 
			gb=nt;
			p++,p++;
			if(abs(p->al-h[i])>=abs(lh-h[i])){
				ga=lt;
			}
			else ga=p->id;
		}
		f[i][0][0]=ga,f[i][0][1]=gb;
		da[i][0][0]=abs(h[ga]-h[i]);
		db[i][0][1]=abs(h[gb]-h[i]);
	}
	for(int i=1;i<=18;i++)
		for(int j=1;j<=n;j++)
			for(int k=0;k<2;k++){
				if(i==1){
					f[j][1][k]=f[f[j][0][k]][0][1-k];
					da[j][1][k]=da[j][0][k]+da[f[j][0][k]][0][1-k];
					db[j][1][k]=db[j][0][k]+db[f[j][0][k]][0][1-k];
				}
				else{
					f[j][i][k]=f[f[j][i-1][k]][i-1][k];
					da[j][i][k]=da[j][i-1][k]+da[f[j][i-1][k]][i-1][k];
					db[j][i][k]=db[j][i-1][k]+db[f[j][i-1][k]][i-1][k];			
				}
			}
}
void work1(int S,int X){
	int p=S;
	la=0;lb=0;
	for(int i=18;i>=0;i--){//a先开车 
		if(f[p][i][0]&&la+lb+da[p][i][0]+db[p][i][0]<=X){//不能越界 
			la+=da[p][i][0];
			lb+=db[p][i][0];
			p=f[p][i][0];
		}
	}
}
int ansid;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",h+i);
	scanf("%d%d",&x0,&m);
	for(int i=1;i<=m;i++)scanf("%d%d",s+i,x+i);
	prework();
	//for(int i=1;i<=n;i++)printf("%d %d %d\n",f[i][0][0],da[i][0][0],db[i][0][1]);
	//system("pause");
	for(int i=1;i<=n;i++){
		work1(i,x0);
		double nowans=(double)la/(double)lb;
		if(nowans<ans){
			ans=nowans;
			ansid=i;
		}
		else if(nowans==ans&&h[ansid]<h[i]){
			ansid=i;
		}
	}
	printf("%d\n",ansid);
	for(int i=1;i<=m;i++){
		work1(s[i],x[i]);
		printf("%d %d\n",la,lb);
	}
	return 0;
}
posted @ 2021-08-12 16:28  ¶凉笙  阅读(180)  评论(0编辑  收藏  举报