DP优化专题

倍增优化DP

在线性DP中,我们一般是按照阶段将DP的状态线性增长,但是我们可以运用倍增思想,将线性增长转化为成倍增长

对于应用倍增优化DP,一般分为两个步骤

1.预处理 ,我们使用成倍增长的DP计算出与二的整次幂有关的代表状态

2.拼凑,根据二进制划分的思想,使用预处理出的状态拼凑出最后的答案(注意,此处一般是倒序循环)

好的下面来一道经典题目详细解释:

[NOIP2012 提高组] 开车旅行

题目描述

A \text{A} A 和小 B \text{B} B 决定利用假期外出旅行,他们将想去的城市从 $1 $ 到 n n n 编号,且编号较小的城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 i i i 的海拔高度为 h i h_i hi,城市 i i i 和城市 j j j 之间的距离 d i , j d_{i,j} di,j 恰好是这两个城市海拔高度之差的绝对值,即 d i , j = ∣ h i − h j ∣ d_{i,j}=|h_i-h_j| di,j=hihj

旅行过程中,小 A \text{A} A 和小 B \text{B} B 轮流开车,第一天小 A \text{A} A 开车,之后每天轮换一次。他们计划选择一个城市 s s s 作为起点,一直向东行驶,并且最多行驶 x x x 公里就结束旅行。

A \text{A} A 和小 B \text{B} B 的驾驶风格不同,小 B \text{B} B 总是沿着前进方向选择一个最近的城市作为目的地,而小 A \text{A} A 总是沿着前进方向选择第二近的城市作为目的地(注意:本题中如果当前城市到两个城市的距离相同,则认为离海拔低的那个城市更近)。如果其中任何一人无法按照自己的原则选择目的城市,或者到达目的地会使行驶的总距离超出 x x x 公里,他们就会结束旅行。

在启程之前,小 A \text{A} A 想知道两个问题:

1、 对于一个给定的 x = x 0 x=x_0 x=x0,从哪一个城市出发,小 A \text{A} A 开车行驶的路程总数与小 B \text{B} B 行驶的路程总数的比值最小(如果小 B \text{B} B 的行驶路程为 0 0 0,此时的比值可视为无穷大,且两个无穷大视为相等)。如果从多个城市出发,小 A \text{A} A 开车行驶的路程总数与小 B \text{B} B 行驶的路程总数的比值都最小,则输出海拔最高的那个城市。

2、对任意给定的 x = x i x=x_i x=xi 和出发城市 s i s_i si,小 A \text{A} A 开车行驶的路程总数以及小 B \text B B 行驶的路程总数。

输入格式

第一行包含一个整数 n n n,表示城市的数目。

第二行有 n n n 个整数,每两个整数之间用一个空格隔开,依次表示城市 1 1 1 到城市 n n n 的海拔高度,即 h 1 , h 2 . . . h n h_1,h_2 ... h_n h1,h2...hn,且每个 h i h_i hi 都是互不相同的。

第三行包含一个整数 x 0 x_0 x0

第四行为一个整数 m m m,表示给定 m m m s i s_i si x i x_i xi

接下来的 m m m 行,每行包含 2 2 2 个整数 s i s_i si x i x_i xi,表示从城市 s i s_i si 出发,最多行驶 x i x_i xi 公里。

输出格式

输出共 m + 1 m+1 m+1 行。

第一行包含一个整数 s 0 s_0 s0,表示对于给定的 x 0 x_0 x0,从编号为 s 0 s_0 s0 的城市出发,小 A \text A A 开车行驶的路程总数与小 B \text B B 行驶的路程总数的比值最小。

接下来的 m m m 行,每行包含 2 2 2 个整数,之间用一个空格隔开,依次表示在给定的 s i s_i si x i x_i xi 下小 A \text A A 行驶的里程总数和小 B \text B B 行驶的里程总数。

【数据范围与约定】

对于 30 % 30\% 30% 的数据,有 1 ≤ n ≤ 20 , 1 ≤ m ≤ 20 1\le n \le 20,1\le m\le 20 1n20,1m20
对于 40 % 40\% 40% 的数据,有 1 ≤ n ≤ 100 , 1 ≤ m ≤ 100 1\le n \le 100,1\le m\le 100 1n100,1m100
对于 50 % 50\% 50% 的数据,有 1 ≤ n ≤ 100 , 1 ≤ m ≤ 1000 1\le n \le 100,1\le m\le 1000 1n100,1m1000
对于 70 % 70\% 70% 的数据,有 1 ≤ n ≤ 1000 , 1 ≤ m ≤ 1 0 4 1\le n \le 1000,1\le m\le 10^4 1n1000,1m104
对于 100 % 100\% 100% 的数据: 1 ≤ n , m ≤ 1 0 5 1\le n,m \le 10^5 1n,m105 − 1 0 9 ≤ h i ≤ 1 0 9 -10^9 \le h_i≤10^9 109hi109 1 ≤ s i ≤ n 1 \le s_i \le n 1sin 0 ≤ x i ≤ 1 0 9 0 \le x_i \le 10^9 0xi109
数据保证 h i h_i hi 互不相同。

分析:我们先预处理出 g a ( i ) , g b ( i ) ga(i),gb(i) ga(i),gb(i)表示从城市i出发下一步小A和小B分别会开往哪一个城市,这个可以通过平衡树实现

仔细阅读题面,我们会发现,本题有三个信息:1.天数,2.城市,3.小A和小B分别行驶的路程总长度

于是我们仔细思考可以发现,当我们知道了行驶天数和出发城市之后,我们肯定可以知道小A和小B分别行驶的路径总长度以及终点城市
于是这启发我们使用动态规划:但是观察 1 0 5 10^5 105的数据范围,这就必须使用上优化,于是我们考虑使用倍增优化DP

具体的,我们设 F i , j , k F_{i,j,k} Fi,j,k表示在第 2 i 2^i 2i天,从城市j出发,当前是k(0A1B)在开车的终点城市

初值: F 0 , i , 0 = g a ( i ) , F 0 , i , 1 = g b ( i ) F_{0,i,0}=ga(i),F_{0,i,1}=gb(i) F0,i,0=ga(i),F0,i,1=gb(i)

由于当 i = 1 i=1 i=1的时候两人是一人一天,于是

F 1 , i , 0 = F 0 , F 0 , i , 0 , 1 , F 1 , i , 1 = F 0 , F 0 , i , 1 , 0 F_{1,i,0}=F_{0,F_{0,i,0},1},F_{1,i,1}=F_{0,F_{0,i,1},0} F1,i,0=F0,F0,i,0,1,F1,i,1=F0,F0,i,1,0

i ≥ 2 i\ge 2 i2时:

F i , j , k = F i − 1 , F i − 1 , j , k , k F_{i,j,k}=F_{i-1,F_{i-1,j,k},k} Fi,j,k=Fi1,Fi1,j,k,k

此时我们就可以通过递推求出 F F F了,那么还有两个需要知道的信息,即小A和小B的路径长度

于是我们设 d a i , j , k , d b i , j , k da_{i,j,k},db_{i,j,k} dai,j,k,dbi,j,k分别表示小A小B在前 2 i 2^i 2i天从城市 j j j出发当前是 k k k在开车的路径长度

与F数组相似的,我们有

初值: d a 0 , i , 0 = d i s t ( i , g a ( i ) ) , d b 0 , i , 1 = d i s t ( i , g b ( i ) ) da_{0,i,0}=dist(i,ga(i)),db_{0,i,1}=dist(i,gb(i)) da0,i,0=dist(i,ga(i)),db0,i,1=dist(i,gb(i))其余全部为0

i = 1 i=1 i=1时有:

d a 1 , i , k = d a 0 , i , k + d a 0 , F 0 , i , k , 1 − k , d b 1 , i , k = d b 0 , i , k + d b 0 , F 0 , i , k , 1 − k da_{1,i,k}=da_{0,i,k}+da_{0,F_{0,i,k},1-k},db_{1,i,k}=db_{0,i,k}+db_{0,F_{0,i,k},1-k} da1,i,k=da0,i,k+da0,F0,i,k,1k,db1,i,k=db0,i,k+db0,F0,i,k,1k

i ≥ 2 i\ge 2 i2时有:

d a i , j , k = d a i − 1 , j , k + d a i − 1 , F i − 1 , j , k , k , d a i , j , k = d a i − 1 , j , k + d a i − 1 , F i − 1 , j , k , k da_{i,j,k}=da_{i-1,j,k}+da_{i-1,F_{i-1,j,k},k},da_{i,j,k}=da_{i-1,j,k}+da_{i-1,F_{i-1,j,k},k} dai,j,k=dai1,j,k+dai1,Fi1,j,k,k,dai,j,k=dai1,j,k+dai1,Fi1,j,k,k

对此进行DP,我们可以在 O ( n log ⁡ n ) O(n\log n) O(nlogn)的时间内得到关于2的整次幂的信息

下面我们考虑询问 c a l c ( s , x ) calc(s,x) calc(s,x)表示从城市s出发最多行走x时小A和小B分别走的路程总数

l a la la为小A路程总数, l b lb lb为小B路程总数

1.我们按照二进制从大到小枚举2的整次幂,记作 i i i,初始化当前城市 p = s p=s p=s

2.若

l a + l b + d a i , p , 0 + d b i , p , 0 ≤ x la+lb+da_{i,p,0}+db_{i,p,0}\le x la+lb+dai,p,0+dbi,p,0x

则令 l a + = d a i , p , 0 , l b + = d b i , p , 0 , p = F i , p , 0 la+=da_{i,p,0},lb+=db_{i,p,0},p=F_{i,p,0} la+=dai,p,0,lb+=dbi,p,0,p=Fi,p,0

循环结束后 l a , l b la,lb la,lb即为所求

枚举起点计算就可以解决问题一,问题二就等同于计算多次 c a l c ( s i , x i ) calc(s_i,x_i) calc(si,xi)


#define int long long;
struct node{
	int val,f,ch[2],id;
}t[100005]
int root,tot;
#define lc(x) t[x].ch[0];
#define rc(x) t[x].ch[1];
void rotate(int x){
	int y=t[x].f,z=t[y].f,k=rc(y)==x;
	t[z].ch[rc(z)==y]=x;
	t[x].f=z;
	t[y].ch[k]=t[x].ch[k^1];
	t[t[y].ch[k]].f=y;
	t[y].f=x;
	t[x].ch[k^1]=y;
}
void splay(int x,int goal=0){
	while(t[x].f!=goal){
		int y=t[x].f,z=t[y].f;
		if(z!=goal){
			rc(y)==x^rc(z)==y?rotate(x):rotate(y);
		}
		rotate(x);
	}
	if(!goal)root=x;
}
void insert(int id,int val){
	int p=0,u=root;
	while(u)p=u,u=t[u].ch[t[u].val<val];
	u=++tot;
	if(!p){
		root=u;
		t[u].f=0;
	}
	else{
		t[p].ch[t[p].val<val]=u;
		t[u].f=p;
	}
	t[u].ch[0]=t[u].ch[1]=0t[u].id=id,t[u].val=val;
	splay(u);
}
int nxt(int val,int p){//0前驱1后继 
	int u=root;
	while(t[u].ch[val>t[u].val]&&t[u].val!=val)u=t[u].ch[t[u].val<val];
	splay(u);
	u=t[u].ch[p];
	while(t[u].ch[!p])u=t[u].ch[!p];
	return u;
}
/*---------------以上是平衡树Splay求前驱后继----------------*/
#define N 100005
#define INF 0x7f7f7f7f7f7f
int n,m,h[N],x[N],s[N],ga[N],gb[N],w;
int f[18][N][2],da[18][N][2],db[18][N][2],la,lb;
void calc(int S,int X) {
	la=lb=0;
	int p=S;
	for(int i=w;i>=0;i--)
		if(f[i][p][0]&&la+lb+da[i][p][0]+db[i][p][0]<=X) {
			la+=da[i][p][0];
			lb+=db[i][p][0];
			p=f[i][p][0];
		}
}
signed main(){
//	freopen("P1081_2.in","r",stdin)
	scanf("%lld",&n);
	insert(0,-INF);
	insert(0,INF-1);
	insert(0,INF);
	insert(0,1-INF);/*插入哨兵方便计算*/
	for(int i=1;i<=n;i++){
		scanf("%lld",&h[i]);
	}
	scanf("%lld%lld",&x[0],&m);
	for(int i=1i<=mi++)scanf("%lld%lld",&s[i],&x[i]);
	w=log(n)/log(2);
	for(int i=n;i>0;--i){
		insert(i,h[i]);
		int hj1=nxt(h[i],1);
		int hj2=nxt(t[hj1].val,1);
		int qq1=nxt(h[i],0);
		int qq2=nxt(t[qq1].val,0);
		int a=t[hj1].id==0?INF:t[hj1].val-h[i];
		int b=t[qq1].id==0?INF:h[i]-t[qq1].val;//小A次小,小B最小 
		if(b<=a){
			gb[i]=t[qq1].id;
			b=t[qq2].id==0?INF:h[i]-t[qq2].val;
			ga[i]=b<=a?t[qq2].id:t[hj1].id;//大小关系一定注意
		}
		else {
			gb[i]=t[hj1].id;
			a=t[hj2].id==0?INF:t[hj2].val-h[i];
			ga[i]=b<=a?t[qq1].id:t[hj2].id;
		}
	}
	for(int i=1;i<=n;i++)f[0][i][0]=ga[i],f[0][i][1]=gb[i];
	for(int i=1;i<=n;i++)f[1][i][1]=f[0][f[0][i][1]][0],f[1][i][0]=f[0][f[0][i][0]][1];
	for(int i=2;i<w;i++){
		for(int j=1;j<=n;j++){
			f[i][j][0]=f[i-1][f[i-1][j][0]][0];
			f[i][j][1]=f[i-1][f[i-1][j][1]][1];
		}
	}
	for(int i=1;i<=n;i++){
		da[0][i][0]=abs(h[i]-h[ga[i]]);
		db[0][i][0]=0;
		da[0][i][1]=0;
		db[0][i][1]=abs(h[i]-h[gb[i]]);
	}
	for(int i=1;i<=n;i++){
		da[1][i][0]=da[0][i][0];
		db[1][i][0]=db[0][f[0][i][0]][1];
		da[1][i][1]=da[0][f[0][i][1]][0];
		db[1][i][1]=db[0][i][1];
	}
	for(int i=2;i<w;i++){
		for(int j=1;j<=n;j++){
			da[i][j][0]=da[i-1][j][0]+da[i-1][f[i-1][j][0]][0];
			da[i][j][1]=da[i-1][j][1]+da[i-1][f[i-1][j][1]][1];
			db[i][j][0]=db[i-1][j][0]+db[i-1][f[i-1][j][0]][0];
			db[i][j][1]=db[i-1][j][1]+db[i-1][f[i-1][j][1]][1];
		}
	}
	/*如分析所言DP*/
	calc(1,x[0]);
	double ans=(lb?(double)la/lb:2e9);
	int num=1;
	for (int i=2;i<=n;i++) {
		calc(i,x[0]);
		if ((double)la/lb<ans||(((double)la/lb==ans)&&h[i]>h[num])){
			num=i;
			ans=(double)la/lb;
		}
	}
	printf("%lld\n",num);
	for(int i=1;i<=m;i++){
		calc(s[i],x[i]);
		printf("%lld %lld\n",la,lb);
	}
}

最后,使用倍增优化DP的前提条件是问题的答案具有强可拼接性,即我们划分阶段做出决策的时候可以任意划分不影响答案(对划分有着限制,比如某个阶段不可以拼凑答案就不可以使用),这样我们就可以把答案的计算变成二的整次幂。

多次查询的dp问题:一般数据结构维护,重复计算很多的话可以倍增优化

数据结构优化DP

在DP过程中有着阶段,状态,决策三个步骤,我们之前的倍增DP和状压DP是从阶段设计和状态上入手进行的优化,数据结构优化DP就是从决策方面对DP进行的优化,包括我们下面的单调队列也是如此

思想概述:运用数据结构的功能加速最优决策的寻找与状态的转移

下面附上常用数据结构的功能

数据结构 支持操作 均摊时间复杂度 代码难度 常数 扩展性
线段树 维护区间信息(可加性),区间修改 单次 O ( log ⁡ n ) O(\log n) O(logn) 一般 较大 较好
树状数组 维护前缀和,区间前缀最值,单点修改 单次 O ( log ⁡ n ) O(\log n) O(logn) 很小 不好
平衡树 维护最值,前驱后继,删除节点,rank 单次期望 O ( log ⁡ n ) O(\log n) O(logn) 较大 较大 较好
Splay ⁡ \operatorname{Splay} Splay 关于序列70%的问题,平衡树操作,区间操作 单次期望 O ( log ⁡ n ) O(\log n) O(logn) 略大 较大
堆(KaTeX parse error: Expected 'EOF', got '_' at position 30: …prioprity\text{_̲}queue}) 插入,删除(STL不支持,懒惰删除法),最值 单次 O ( log ⁡ n ) O(\log n) O(logn) 手写较大 很小 不好
分块 区间几乎所有问题 单次 O ( n ) O(\sqrt{n}) O(n ) 一般 较小 极好
树套树 取决于嵌套的结构,一般为功能总和 单次 O ( log ⁡ 2 n ) O(\log^2 n) O(log2n)+ 极大(用得少) 较好
(可持久化)trie 关于异或的操作(区间操作需要可持久化) 单次 O ( log ⁡ V ) O(\log V) O(logV)V为值域 较小 较小 一般
可持久化线段树 线段树操作(除区间修改),区间rank,历史版本查询 单次 O ( log ⁡ n ) O(\log n) O(logn) 较小 较小 一般

注:
1.普通线段树需要 4 n 4n 4n空间,可持久化数据结构需要一般形态的十倍至二十倍
2.线段树和树状数组可以经过离散化开值域上的,不仅仅限于序列上的

在状态转移方程中,如遇到某些数据结构支持的操作,便可以使用数据结构进行优化,下面给几个例题感受一下

清理班次2

题目描述:有 N N N头奶牛,每一头奶牛在 a i ∼ b i a_i\sim b_i aibi个班次内工作,需要付出代价 c i c_i ci,求使 [ M , E ] [M,E] [M,E]的班次内都有奶牛在工作的最小代价

由题:设 F i F_i Fi表示 M ∼ i M\sim i Mi的班次内的最小代价,明显有状态转移方程:

F b i = min ⁡ a i − 1 ≤ j ≤ b i − 1 F j + c i F_{b_i}=\min_{a_i-1\le j \le b_i-1}F_j+c_i Fbi=ai1jbi1minFj+ci

初值: F M − 1 = 0 F_{M-1}=0 FM1=0,其余均为正无穷,目标 min ⁡ i ≥ E F i \min_{i\ge E}{F_i} miniEFi,实现时需要注意边界

于是我们观察状态转移方程发现,我们需要查询一个区间内的最小值,而朴素写法无疑需要 O ( n ) O(n) O(n)的时间,这一步就可以采用线段树进行优化

  memset(f,0x3f,sizeof f);
	int S,T;
	scanf("%d%d%d",&n,&S,&T);
	if(T>0)build(1,1,T);
	if(T>0)f[0]=0;
	int sum=0;
	for(int i=1;i<=n;i++)scanf("%d%d%d",&ask[i].x,&ask[i].y,&ask[i].z),sum+=ask[i].z;
	sort(ask+1,ask+n+1,cmp) ;
	for(int i=1;i<=n;i++){
		int mn=ask[i].x-1<=S?0:query(1,ask[i].x-1,min(T,ask[i].y-1));
		f[ask[i].y]=min(mn+ask[i].z,f[ask[i].y]);
		if(ask[i].y>0)update(1,ask[i].y,f[ask[i].y]) ;
	}
	printf("%d\n",f[T]>sum?-1:f[T]);
赤壁之战

题意:给定一个长度为 N N N的序列 A A A,求 A A A中长度为 M M M的严格递增子序列的个数

由题,很容易想到

F i , j F_{i,j} Fi,j表示 A A A中前 i i i个数以 A i A_i Ai为结尾的组成长度为 j j j的严格递增子序列的个数

不难得出状态转移方程:

F i , j = ∑ k < i , A k < A i F k , j − 1 F_{i,j}=\sum_{k<i,A_k<A_i}F_{k,j-1} Fi,j=k<i,Ak<

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值