决策单调性优化 dp

决策单调性

我们宣称形如 \(\displaystyle f_i=\min_{j=1}^{i-1}g_j+w(j,i)\) 的转移式是前缀转移。

\(\forall i_1<i_2\),对于 \(i_1\) 的所有转移点 \(j_1\) 均存在一个 \(j_2\) 使得 \(j_2\)\(i_2\) 的转移点且 \(j_2>j_1\),那我们称这个 dp 式满足决策单调性。

四边形不等式

\(w(j,i)\) 为从 \(j\) 转移到 \(i\) 的贡献,那么若 \(\forall a\le b\le c\le d\)\(w(a,c)+w(b,d)\) 优于 \(w(a,d)+w(b,c)\),我们称 \(w\) 满足四边形不等式(相交优于包含)。

一个结论是,若 \(w\) 满足四边形不等式,则该 dp 有决策单调性。

我们可以对四边形不等式进行一些等价变形,不妨设更小的为优,即 \(w(b,d)-w(b,c)\le w(a,d)-w(a,c)\)

于是有 \((g[b]-w(b,d))-(g[b]+w(b,c))\le (g[a]-w(a,d))-(g[a]+w(a,c))\),换句话说,离决策点越远的点,只会变得越来越劣,即当它不再是决策点时,它以后也不可能是决策点。

这也就证明了上面的结论。

关于如何发现四边形不等式,我们一般在无法对转移进行任何优化的情况下,对 \(w\) 打表并观察是否满足。

分治法优化

\(g=f\),我们称该种转移为自转移,否则为他转移。分治法优化只能在他转移是使用。

对于一个区间 \([l,r]\),我们找到中点 \(mid\),只需暴力计算所有 \([L,\min(mid-1,R)]\) 的贡献即可算出 \(f_{mid}\),同时确定 \([l,mid-1],[mid+1,r]\) 的决策点选择区间。

注意到每层暴力扫描部分的总和为 \(n\),而分治只有 \(\log n\) 轮,故复杂度为 \(O(n\log n)\)

P3515

不妨设 \(j<i\),扫两遍即可。

\(f_i\) 表示 \(i\) 处的答案,显然你只需要知道 \(i\) 前面的所有 \(h\) 就能求出来 \(f_i\)。换句话说这是个他转移,可以上分治优化。

于是直接设 \(\operatorname{solve}(l,r,L,R)\) 表示当前要求 \([l,r]\) 内的 \(f\) 值,决策区间为 \([L,R]\),每次暴力求出 \(f_{mid}\) 并找到决策点即可。

注意 \(f\) 事实上是一个浮点数,如果在计算过程中将 \(f\) 取整的话可能会将 \(3.2\)\(3.3\) 认为是同一个数,导致决策点错误。所以 \(f\) 不能在计算过程中取整。

代码

CF321E

显然可以设出 \(f_{k,i}\) 表示前 \(i\) 个数分成 \(k\) 段的最小代价。暴力转移 \(f_{k,i}=\min(f_{k-1,j}+w(j,i))\),显然 \(w\) 满足四边形不等式,所以该 dp 有决策单调性。又因为转移不同层所以是他转移,直接上分治优化即可。

代码

二分队列法优化

注意到分治法只能优化他转移,那么自转移怎么优化呢?

比方说我们现在正在决策 \(i\),维护了一个决策队列 \(j_1,j_2,\cdots,j_m\),然后我们称 \(x_i\)\(j_i\)\(j_{i-1}\) 的分界线(分界线以前用 \(j_{i-1}\),否则用 \(j_i\))。

那么,如果有 \(x_i\le x_{i-1}\),那么 \(j_{i-1}\) 就被两边薄纱了,直接把它扔掉就好。

否则,显然存在一个分界线,分界线前用 \(x_{i-1}\) 更好,分界线后用 \(x_i\) 更好。不难发现这个东西可以二分。

所以我们的策略就是,在转移 \(i\) 时,把所有已经够不到 \(i\) 的队首弹出,然后用队首转移。接下来插入 \(i\) 这个决策点,然后不断弹出队尾被两面薄纱的,最后二分出来 \(i\) 和队尾的决策分界线并修改,最后插入 \(i\) 在队尾。

类似二分队列,我们还有二分栈,但这里不再进行讲解,因为原理比较相似。

P1912

首先注意到 \(w\) 满足四边形不等式,所以有决策单调性。

然后转移是一个超级典的前缀转移,类型为自转移,上二分队列即可。因为值域比较大,建议使用精度较高的浮点数类型(比如 long double)。

代码

斜率优化

有的时候,\(w(j,i)\) 形如 \(a(i)+b(j)+c(i)d(j)\)。我们给出结论,若 \(c(i),d(j)\) 具有相反的单调性,则 \(w\) 满足四边形不等式(能做到线性)。

你发现 \(j\) 处的转移相当于求 \(b(j)+d(j)x\)\(c(j)\) 处的值,所以这是一条直线(有时是线段),于是你可以把二分队列变成单调队列维护上凸壳/下凸壳,时间复杂度变为 \(O(n)\)

但是,如果什么都不满足呢?这时我们可以使用强力的维护直线(线段)的数据结构:李超线段树,其插入直线的复杂度为 \(O(\log n)\),插入线段的复杂度为 \(O(\log^2 n)\),并且支持查询一个位置所有直线(线段)的极值。

P3195

其实这个题就是上面那个题的特殊性质。只不过我们发现其满足更好的性质,可以做到线性。

首先把平方式子拆开,然后把它弄成上面的 \(a(i)+b(j)+c(i)d(j)\) 形式,发现是可以直接上单调队列的,然后就没了(注意一些细节位置有没有 \(+1,-1\) 等常数)。

代码

P14388

上面的题还是太弱了,我们来看一些更厉害的题吧。

斜率优化的第一部显然是写出暴力 dp 然后观察转移形式。然后你发现你不会写暴力 dp。

你发现,我们要做的事情相当于在两站中间赶一些人下车。注意到,你每次赶的人都是连续的一段,且必须在到站前的最后一轮开始赶人(否则司机就被你赶下去了)。

于是我们把所有乘客按照 \(D\) 排序,这样我们每次就会赶下去一个区间,所以就可以 dp 了。设 \(f_i\) 表示前 \(i\) 个人,赶下去了不知道几个人,然后 \(i\) 是这一次赶人(如果真的赶了)的赶下去的最后一个。

显然,我们可以预处理每个人最早在第几个车站之前能作为最后一个被赶下去的人被赶下去,这个只需把所有 \(S\) 拉出来,然后在 \(D\) 上二分。

显然我们有转移 \(f_i=f_{i-1}+W\times (\lfloor\dfrac{X-D_i}{T}+1\rfloor)\)。这个表示不把 \(i\) 赶下去,所以我们要付他的所有水费。

我们设 \(g_i\) 表示 \(i\) 被赶下去时,他至少接了多少水。所以有转移 \(f_i=f_k+\sum_{p=k+1}^{i}C_p+W\times t_i\),对所有 \(k\) 取最小值即可。

于是我们做到了 \(O(n^2)\)

显然,不把 \(i\) 赶下去的转移是简单的。然后你设 \(sc\)\(C\) 的前缀和,改写一下转移形式即可得到:\(f_i-sc_i-W\times i\times t_i=-W\times k\times t_i+f_k-sc_k\)

你发现,这个东西就是在查询,\(x=t_i\) 时一条直线的最小值啊,直接扔一个李超线段树上去就过了。

代码

区间转移

我们宣称形如 \(\displaystyle f_{l,r}=\min_{j=1}^{i-1}f_{l,k}+f_{k+1,r}+w(l,r)\) 的转移式是区间转移。

此时的四边形不等式的定义在上方的基础上要加上一条,\(\forall L\le l\le r\le R\)\(w(l,r)\) 优于 \(w(L,R)\),即小区间优于大区间。

此时决策单调性的定义类似,不再赘述。只需记住,有 \(pos_{l,r-1}\le pos_{l,r}\le pos_{l+1,r}\)

然后你这样就可以把一个朴素为 \(O(n^3)\) 的区间 dp 优化到 \(O(n^2)\)

P1880

在最小值时 \(w\) 显然满足四边形不等式,所以直接上优化即可。

但是在最大值是 \(w\) 不满足小区间优于大区间,所以 \(w\) 满足四边形不等式,也就具有决策单调性。

但是不知道为什么,在求最大值时,从 \(l\)\(r-1\) 转移求出的答案是对的,所以最终仍然是 \(O(n^2)\) 的(不会证明)。

注意最大值的转移可能是假掉的,不要把此当一个结论来记。

代码

HDU7097

超级好题。

首先考虑没有修改怎么做,不难发现 \(w\) 满足四边形不等式,所以有决策单调性,所以可以 \(O(n^2)\) dp。

首先建出修改树并在上面 dfs,可以省去很多麻烦。

但是,如果我们有修改,因为决策单调性是用均摊保证的复杂度,所以我们不能每次修改后直接插入。

首先,二维前缀和数组我们是可以用 \(O(m)\) 的复杂度来完成修改的。

我们好像忘记了一些其他东西,你发现你只需要维护所有的 \(f_{l,N}\),其中 \(N\) 是当前 \(a\) 的长度,这个东西你把它的转移写出来,你发现它是一个前缀转移,形式为自转移,所以我们可以上二分队列。

然后你只需要写出一大坨代码并把它调出来就好了(注意 HDU 上不能用万能头)。

下面放一下代码,因为我没在 HDU 上面交。

点击查看代码
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#define ll long long
#define N 3005
#define K 305
#define mod 1000000007
#define B 13331
#define pii pair<int,int>
#define x first
#define y second
#define pct __builtin_popcount
#define mpi make_pair
#define inf 2e18
#define eps 1e-10
using namespace std;
int T=1,n,m,r,pos[N][N],a[K][N],cnt[K];
ll dis[N][N],s[N][N],f[N][N];
vector<pii>e[N];
void add(int a,int b,int c){
	e[a].push_back({b,c});
}
void dfs(int u,int fa,int rt){
	for(auto it:e[u]){
		int j=it.x,w=it.y;
		if(j==fa)continue;
		dis[rt][j]=dis[rt][u]+w;
		dfs(j,u,rt);
	}
}
ll qry(int x_1,int y_1,int x_2,int y_2){
	if(x_1>x_2||y_1>y_2)return 0;
	return s[x_2][y_2]-s[x_1-1][y_2]-s[x_2][y_1-1]+s[x_1-1][y_1-1];
}
struct node{
	int pos,l,r;
}q[N];
void init(int t){
	ll sum=0;
	for(int i=cnt[t];i<=cnt[t];i++){
		for(int j=1;j<cnt[t];j++){
			s[i][j]=dis[a[t][i]][a[t][j]]+sum+qry(1,1,cnt[t]-1,j);
			sum+=dis[a[t][i]][a[t][j]];
		}
	}
	sum=0;
	for(int i=1;i<cnt[t];i++){
		for(int j=cnt[t];j<=cnt[t];j++){
			s[i][j]=dis[a[t][i]][a[t][j]]+sum+qry(1,1,i,cnt[t]-1);
			sum+=dis[a[t][i]][a[t][j]];
		}
	}
	s[cnt[t]][cnt[t]]=qry(1,1,cnt[t]-1,cnt[t])+qry(1,1,cnt[t],cnt[t]-1)-qry(1,1,cnt[t]-1,cnt[t]-1)+dis[a[t][cnt[t]]][a[t][cnt[t]]];
}
ll w(int i,int p,int j){
	return f[i][p]+f[p+1][j]+qry(i,i,j,j);
}
ll res[K];
struct Qry{
	vector<pii>e[K];
	void clear(){
		for(int i=0;i<K;i++){
			e[i].clear();
		}
	}
	void add(int a,int b,int c){
		e[a].push_back({b,c});
	}
	void solve(int x,int t,int y){
		cnt[t]=cnt[x];
		for(int i=1;i<=cnt[t];i++){
			a[t][i]=a[x][i];
		}
		a[t][++cnt[t]]=y;
		init(t);
		f[cnt[t]][cnt[t]]=0;
		pos[cnt[t]][cnt[t]]=cnt[t];
		int hh=1,tt=0;
		q[++tt]={cnt[t],1,cnt[t]};
		for(int i=cnt[t]-1;i;i--){
			while(hh<=tt&&q[hh].l>i)hh++;
			if(q[hh].r>=i)q[hh].r=i;
			if(q[tt].r>=i)q[tt].r=i;
			int pos=cnt[t]+1;
			while(hh<=tt&&(q[tt].l>q[tt].r||w(q[tt].r,q[tt].pos,cnt[t])>=w(q[tt].r,i,cnt[t]))){
				pos=q[tt].r;
				tt--;
				if(q[tt].r>=i)q[tt].r=i;
			}
			if(hh>tt)pos=i;
			if(hh<=tt&&w(q[tt].l,q[tt].pos,cnt[t])>=w(q[tt].l,i,cnt[t])){
				int l=q[tt].l,r=q[tt].r,res=q[tt].l;
				while(l<=r){
					int mid=l+r>>1;
					if(w(mid,q[tt].pos,cnt[t])>=w(mid,i,cnt[t])){
						l=mid+1;
						res=mid;
					}
					else r=mid-1;
				}
				pos=res;
				q[tt].l=res+1;
			}
			if(pos!=cnt[t]+1)q[++tt]={i,1,pos};
			f[i][cnt[t]]=w(i,q[hh].pos,cnt[t]);
		}
		res[t]=f[1][cnt[t]];
	}
	void dfs(int u){
		for(auto it:e[u]){
			int j=it.x,w=it.y;
			solve(u,j,w);
			dfs(j);
		}
	}
}Q;
void solve(int cs){
	if(!cs)return;
	Q.clear();
	for(int i=0;i<K;i++){
		cnt[i]=0;
	}
	for(int i=0;i<N;i++){
		e[i].clear();
		for(int j=0;j<N;j++){
			f[i][j]=inf;
			dis[i][j]=0;
		}
	}
	cin>>m>>n>>r;
	for(int i=1;i<=m;i++){
		int x,y;
		cin>>x>>y;
		add(x,i,y);
		add(i,x,y);
	}
	for(int i=0;i<=m;i++){
		dfs(i,m+1,i);
	}
	cnt[0]=n;
	for(int i=1;i<=n;i++){
		cin>>a[0][i];
		f[i][i]=0;
		pos[i][i]=i;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			s[i][j]=dis[a[0][i]][a[0][j]];
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			s[i][j]+=s[i-1][j];
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			s[i][j]+=s[i][j-1];
		}
	}
	for(int len=2;len<=n;len++){
		for(int l=1,r=len;r<=n;l++,r++){
			for(int k=max(l,pos[l][r-1]);k<=min(r-1,pos[l+1][r]);k++){
				ll val=f[l][k]+f[k+1][r]+qry(l,l,r,r);
				if(f[l][r]>val){
					f[l][r]=val;
					pos[l][r]=k;
				}
			}
		}
	}
	cout<<f[1][n]<<'\n';
	for(int t=1;t<=r;t++){
		int x,y;
		cin>>x>>y;
		Q.add(x,t,y);
	}
	Q.dfs(0);
	for(int t=1;t<=r;t++){
		cout<<res[t]<<'\n';
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	// init();
	cin>>T;
	for(int cs=1;cs<=T;cs++){
		solve(cs);
	}
	// cerr<<clock()*1.0/CLOCKS_PER_SEC<<'\n';
	return 0;
}
posted @ 2025-12-22 22:10  zxh923  阅读(4)  评论(0)    收藏  举报