20250725 决策单调性DP优化一类,学习笔记

斜率优化

这里不讲

四边形不等式

图片

四边形不等式:如果对于任意 \(a\leq b\leq c\leq d\) 均成立

$ w(a,c)+w(b,d) \leq w(a,d)+w(b,c), $

则称函数 w 满足四边形不等式

利用决策单调性,有两种常见算法可以将算法复杂度优化到 O(n\log n) 。

分治

要求解所有状态,只需要求解所有最优决策点。为了对所有
[1 \leq i \leq n] 求解 [\mathop{\mathrm{opt}}(i)] ,首先计算 [\mathop{\mathrm{opt}}(n/2)] ,而后分别计算 [1 \leq i < n/2] 和 [n/2 < i \leq n] 上的 [\mathop{\mathrm{opt}}(i)] ,注意此时已知前半段的 [\mathop{\mathrm{opt}}(i)] 必然位于 [1] 和 [\mathop{\mathrm{opt}}(n/2)] 之间(含端点),而后半段的 [\mathop{\mathrm{opt}}(i)] 必然位于 [\mathop{\mathrm{opt}}(n/2)] 和 [\mathop{\mathrm{opt}}(n)] 之间(含端点)。对于两个子区间,也类似处理,直至计算出每个问题的最优决策。在分治的过程中记录搜索的上下边界,就可以保证算法复杂度控制在 [O(n\log n)] 。递归树层数为 [O(\log n)] ,而每层中,单个决策点至多计算两次,所以总的计算次数是 [O(n\log n)] 。

二分队列

注意到对于每个决策点
[j] ,能使其成为最小最优决策点的问题 [i] 必然构成一个区间。可以通过单调队列记录到目前为止每个决策点可以解决的问题的区间,这样,问题的最优解自然可以通过队列中记录的决策点计算得到。


显然分治是离线的,二分队列是在线的。

我更喜欢分治。


例题:

P3515 [POI 2011] Lightning Conductor

p=⌈max{aj​+∣i−j∣}⌉−ai​,

图片

发现w满足四边形不等式,所以直接编码即可,可以用正反都做一次去掉绝对值。


点击查看代码
#include<bits/stdc++.h>
using namespace std;
constexpr int maxn = 5e5+10;

int n ,a[maxn];
double dp[maxn], sqr[maxn];

inline double w(int j,int i){
	return double(a[j]) + sqr[i - j];
}

void slove(int l,int r,int L,int R){
	if(l > r) return;
	int mid = (l + r) >> 1, p;
	double maxv = 0;
	for(int i = L;i <= min(mid,R);i++){
		if(w(i,mid) > maxv) maxv = w(i,mid), p = i;
	}
	dp[mid] = max(dp[mid],maxv);
	slove(l,mid-1,L,p);
	slove(mid+1,r,p,R);
}

int main(){
	
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i = 1;i <= n;i++){
		cin>>a[i];
		sqr[i] = sqrt(i);
	}
	
	slove(1,n,1,n);
	reverse(dp + 1, dp + n + 1);
	reverse(a + 1, a + n + 1);
	slove(1,n,1,n);
	reverse(dp + 1, dp + n + 1);
	reverse(a + 1, a + n + 1);
	for(int i = 1;i <= n;i++){
		cout<<(int)(ceil(dp[i]) - a[i])<<endl;
	}
}

P4767 [IOI 2000] 邮局 加强版

定义dp[i][j]表示前i个村庄放j个邮局的前i个村庄的最小距离总和,w(i,j)表示村庄区间[i,j]内放一个村庄时该区间的最小距离总和。

p[i][j]=min{dp[k][j−1]+w(k+1,i)},k∈[0,i)

发现w满足四边形不等式

所以记录d[][]为dp[][]的转移。

至于为什么要倒着枚举i,因为计算dp[i][j]的转移状态起始点要依赖d[i+1][j]。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
constexpr int maxn = 3010;
constexpr int INF = 1e9;

int V, P;
int x[maxn], dp[maxn][maxn], w[maxn][maxn], d[maxn][maxn];

signed main(){
	cin>>V>>P;
	for(int i = 1;i <= V;i++){
		cin>>x[i];
	}
	
	for(int l = 1;l <= V;l++){
		w[l][l] = 0;
		for(int r = l + 1;r <= V;r++){
			w[l][r] = w[l][r-1] + x[r] - x[(l + r) / 2];
		}
	}
	
	sort(x+1,x+V+1);

	memset(dp,0x3f, sizeof dp);
	dp[0][0] = 0;
	for(int j = 1;j <= P;j++){
		d[V+1][j] = V;
		for(int i = V;i >= 1;i--){
			int minval = INF, minpos = 0;
			for(int k = d[i][j-1];k <= d[i+1][j];k++){
				if(dp[k][j-1] + w[k+1][i] < minval){
					minval = dp[k][j-1] + w[k+1][i];
					minpos = k;
				}
			}
			dp[i][j] = minval;
			d[i][j] = minpos;
		}
	}
	cout<<dp[V][P]<<endl;
	return 0;
	
}



WQS二分

就是带权二分。

比如,假设要解决的问题是,要从 [n] 个物品中选取 [m] 个,并最优化某个较复杂的目标函数。如果设从前 [i] 个物品中选取 [j] 个,目标函数的最优值为 [f(i,j)] ,那么原问题的答案就是 [f(n,m)] 。这类问题中,状态转移方程通常是二维的。直接实现该状态转移方程,时间复杂度是

[O(nm)] 的,难以接受。

进一步假设,没有数量限制的最优化问题容易解决。但是,选取到的最优数量未必满足原问题的数量限制。假设选取的物品过多。那么,就可以考虑在选取物品时,为每个选取到的物品都附加一个固定大小的惩罚
[k] (即「带权二分」中的「权」),仍然解没有数量限制的最优化问题。根据 [k] 的取值不同,选取到的最优数量也会有所不同;而且,随着 [k] 的变化,选取到的最优数量也是单调变化的。所以,可以通过二分,找到 [k] 使得选取到的最优数量恰为 [m] 。假设此时目标函数的最优值为 [f_k(n)] ,那么,只要消除额外附加的惩罚造成的价值损失,就能得到原问题的答案 [f(n,m)=f_k(n)+km] 。假设单次求解附加惩罚的问题的复杂度是 [O(T(n))] 的,那么,算法的整体复杂度也就降低到了 [O(T(n)\log L)] ,其中, [O(\log L)] 是二分

[k] 需要的次数。

这就是 WQS 二分的基本想法。但是,这一想法能够行得通,前提是
[f(n,m)] 关于 [m] 是凸的。否则,可能不存在使得最优数量恰为 [m] 的附加惩罚 [k] 。这也是这种 DP 优化方法常常称为「凸优化 DP」或「凸完全单调性 DP」的原因。

例题:

[国家集训队] Tree I

设gx​为强制选x个物品的最大/最小权值和,如果所有的点对(x,gx​)在平面上能够构成一个凸包,那么可以考虑使用WQS二分。

简单的来说,我们不能知道这个凸包长什么样子,但我们可以拿着一个斜率为k的直线去切这个凸包,相当于给每个物品附加了一个权值k。设直线的截距为b,那么选x个物品后总权值就会等于b+kx。我们通过O(n)的DP等方法找到最大的b,同时也可以求出选了的个数x,通过x与need的关系来调整直线斜率继续二分。

拿本题来说,选x条白边,可以写个平方DP然后发现gx​是个下凸函数。然后我们在[−100,100](显然是斜率的上下界,因为更改一条边带来的权值和的更改不会超过100)的范围内二分k,之后所有白边的权值增加k,跑一遍Kruscal统计选了多少条白边。如果这个数量大于等于need就调大k,否则调小。

点击查看代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
struct Edge {
	int u, v, w;
	bool b; 
	double cw;
};

vector<int> p;

int fs(int i) {
	if (p[i] == i) return i;
	return p[i] = fs(p[i]);
}

bool un(int u, int v) {
	int ru = fs(u);
	int rv = fs(v);
	if (ru != rv) {
		p[ru] = rv;
		return true;
	}
	return false;
}

pair<long long, int> ck(int n, int m, vector<Edge>& es, int pn) {
	for (int i = 0; i < m; ++i) {
		es[i].cw = es[i].w + (es[i].b ? 0 : pn);
	}
	
	sort(es.begin(), es.end(), [](const Edge& a, const Edge& b) {
		if (a.cw != b.cw) {
			return a.cw < b.cw;
		}
		return a.b < b.b; 
	});
	
	p.assign(n, 0);
	iota(p.begin(), p.end(), 0);
	
	long long tw = 0; 
	int wc = 0;    
	int ec = 0;      
	
	for (int i = 0; i < m && ec < n - 1; ++i) {
		if (un(es[i].u, es[i].v)) {
			tw += es[i].cw;
			if (!es[i].b) {
				wc++;
			}
			ec++;
		}
	}
	return {tw, wc};
}

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	
	int n, m, nd;
	cin >> n >> m >> nd;
	
	vector<Edge> es(m);
	for (int i = 0; i < m; ++i) {
		cin >> es[i].u >> es[i].v >> es[i].w >> es[i].b;
	}
	
	int lw = -100, hi = 100;
	long long ans_w = 0; 
	
	while (lw <= hi) {
		int mid = lw + (hi - lw) / 2;
		vector<Edge> ces = es; 
		pair<long long, int> res = ck(n, m, ces, mid);
		
		if (res.second >= nd) {
			ans_w = res.first;
			lw = mid + 1;
		} else {
			hi = mid - 1;
		}
	}
	
	cout << ans_w - (long long)nd * hi << endl;
	
	return 0;
}
posted @ 2025-07-25 20:12  Dreamers_Seve  阅读(8)  评论(0)    收藏  举报