[笔记]动态规划优化(斜率优化,决策单调性优化)

本文主要记录某些动态规划思路及动态规划优化。

首先先把以前写过的斜率优化祭出来。

斜率优化

P5017 [NOIP2018 普及组] 摆渡车

经典例题。

fi 表示最后班车在 i 时刻发车,所有人等待时间和的最小值。(这里的所有人是指到达时刻小于等于 i 的所有人)。

容易列出转移方程:

fi=min{fj+cost(j+1,i)}(jim)

其中 cost(i,j) 到达时间在 [i,j] 范围内的所有人等待的时间和。也就是 k(jtk),其中 tk[i,j]

然后考虑如何 O(1) 计算 cost(i,j)。首先拆式子,变成 cost(i,j)=kjktk。设在范围内的 k 共有 c(i,j) 个,在范围内的 tk 的和为 s(i,j),那么就可以 O(1) 计算。而 c,s 可以通过桶上前缀和得到。

具体的,假设 si 为到达时间小于等于 i 的人到达时间的和,ci 表示到达时间小于等于 i 的人的个数,那么 cost(i,j)=j×(cjci1)(sjsi1)

那么这样整个算法复杂度就是 Θ(n2) 的。只能通过 50% 的数据。

接下来引入斜率优化。假设有 j1,j2 两个决策点,且 j2>j1j2 优于 j1。那么有

fj1+cost(j1+1,i)fj2+cost(j2+1,i)

接下来把 cost 展开,得:

fj1+i×(cicj1)(sisj1)fj2+i×(cicj2)(sisj2)

展开,得:

fj1+i×cii×cj1si+sj1fj2+i×cii×cj2si+sj2

两边同时消掉 i×ci,si,得:

fj1i×cj1+sj1fj2i×cj2+sj2

接下来,将含有 i 的项移到左边,其余的移到右边:

i×(cj2cj1)+sj1(fj2+sj2)(fj1+sj1)

假设 cj2cj10,那么可以将 cj2cj1 除到右边:

i(fj2+sj2)(fj1+sj1)(cj2cj1)

f(x)=(fx+sx),那么 if(j2)f(j1)cj2cj1

假设横坐标为 cx,那么不等式右边可以斜率形式。设两个决策点 j1,j2 之间的斜率为 k=f(j2)f(j1)cj2cj1,而待转移点为 i,那么有:

{i<kj2 is better than j1i>kj1 is better than j2i=kj2 is as good as j1

接下来考虑如下情景:

其中 k1>k3。那么可以证明,无论 i 为多少, B 点都是无用状态。

所以最优转移点之间的斜率一定严格单调递增。也就是我们要维护一个下凸壳。可以搞一个单调队列来做,然后每次转移的时候二分找到最佳转移点即可。这里的最佳转移点 j 就是指 j,nextj 之间的斜率为大于等于 i 的最小值。

所以搞完了。代码如下:

int cost(int x, int y) {
	return y * (s[y].first - s[x - 1].first) - 
		   (s[y].second - s[x - 1].second);
}
int f(int x) {
	return dp[x] + s[x].second;
}
double delta(int x1, int x2) {
	if (s[x2].first - s[x1].first == 0) return 1e-9;
	return (s[x2].first - s[x1].first);
}
double slope(int x1, int x2) {
	return (double)(f(x2) - f(x1)) / delta(x1, x2);
}
int get(int S) {
	int l = 1, r = top;
	while (l < r) {
		int mid = l + r >> 1;
		if (slope(stk[mid], stk[mid + 1]) >= S) r = mid;
		else l = mid + 1;
	}
	return stk[l];
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ ) {
		scanf("%d", &t[i]);
		Map[t[i]].first ++ ;
		Map[t[i]].second += t[i];
		maxt = max(maxt, t[i]);
	}
	s[0].first = Map[0].first;
	s[0].second = Map[0].second;
	for (int i = 1; i < maxt + m; i ++ ) {
		s[i].first = s[i - 1].first + Map[i].first;
		s[i].second = s[i - 1].second + Map[i].second;
	}
	
	for (int i = 0; i < maxt + m; i ++ ) {
		if (i >= m) {
			while (top and slope(stk[top - 1], stk[top]) > slope(stk[top], i - m)) top -- ;
			stk[ ++ top] = i - m;
		}
		dp[i] = s[i].first * i - s[i].second;
		if (i >= m) {
			int j = get(i);
			dp[i] = dp[j] + cost(j + 1, i);
		}
	}
	int ans = 2e9;
	for (int i = maxt; i < maxt + m; i ++ )
		ans = min(ans, dp[i]);
	printf("%d\n", ans);
	return 0;
}

接下来考虑线性做法。由于标准斜率 i 也是严格单调递增的,那么就可以把单调栈换成单调队列然后线性转移了。代码如下:

int t[N], dp[N];
int n, m, maxt;
PII Map[N], s[N];
int q[N];

int cost(int x, int y) {
	return y * (s[y].first - s[x - 1].first) - 
		   (s[y].second - s[x - 1].second);
}
int f(int x) {
	return dp[x] + s[x].second;
}
double delta(int x1, int x2) {
	if (s[x2].first - s[x1].first == 0) return 1e-9;
	return (s[x2].first - s[x1].first);
}
double slope(int x1, int x2) {
	return (double)(f(x2) - f(x1)) / delta(x1, x2);
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ ) {
		scanf("%d", &t[i]);
		Map[t[i]].first ++ ;
		Map[t[i]].second += t[i];
		maxt = max(maxt, t[i]);
	}
	s[0].first = Map[0].first;
	s[0].second = Map[0].second;
	for (int i = 1; i < maxt + m; i ++ ) {
		s[i].first = s[i - 1].first + Map[i].first;
		s[i].second = s[i - 1].second + Map[i].second;
	}
	
	int hh = 1, tt = 0;
	for (int i = 0; i < maxt + m; i ++ ) {
		if (i >= m) {
			while (hh < tt and slope(q[tt - 1], q[tt]) > slope(q[tt], i - m)) tt -- ;
			q[ ++ tt] = i - m;
		}
		while (hh < tt and slope(q[hh], q[hh + 1]) <= i) hh ++ ;
		dp[i] = s[i].first * i - s[i].second;
		if (i >= m) dp[i] = dp[q[hh]] + cost(q[hh] + 1, i);
	}
	int ans = 2e9;
	for (int i = maxt; i < maxt + m; i ++ )
		ans = min(ans, dp[i]);
	printf("%d\n", ans);
	return 0;
}

所以总结出斜率优化的规律:

  1. 可以表示成 fi=min(max){fj+cost(j+1,i)} 的形式。

  2. 变形后的标准斜率非单调递增,可以在单调栈 / 单调队列里二分。

  3. 变形后的标准斜率单调递增,可以直接写单调队列暴力转移。

以上的递增也可以换成递减。

如果存在 x 非单调递增的情况,辣么就需要搞平衡树了。

1D 决策单调性优化

原本证明很复杂的样子。实际上证明基本靠打表观察决策点。

决策单调性是指某一类 DP 方程,当自变量 i 单调移动时,其决策点也单调移动。

目前已知的决策单调性满足这样的规律(仅考虑 1D):

  1. 方程一般是 fi=fj+cost(j+1,i) 的形式。

  2. cost(j+1,i) 的关于 i 的二阶偏导恒大于 / 小于零。

我知道首先要证什么 f 满足四边形不等式,还要证明什么 w 满足四边形不等式且局部单调啥的,但是这是规律。学 OI 谁还证明啊/kk

关于 i 的二阶偏导为正的意义就是指 cost 函数随 j 增大增长率越来越快。否则就是增长率越来越慢。这显然和决策单调性挂钩。

举个例子,如果 fi=fj+(ij)2,这个显然是满足决策单调性的。它关于 i 的偏导大概是 2(ij) 的样子。然后可以发现这个决策点是单调左移的。

接下来引入二分队列。对于某些情况,比如下面的例子:

6,3,1

1,2,3

黑色的是初始值,红色的是增长率。求最大值。可以发现,决策点事单调右移的。

然后第一秒的时候,可以发现是第一个数是最优决策点。此时三个数分别为 7,5,4

第二秒的时候,计算机本来以为决策点要向右移动了,但实际上这时候三个数分别为 8,7,7。最优决策点位置不动。

第三秒的时候,计算机觉得第二个数改该为最优决策点了吧?结果还不是。这时候的状态是 9,9,10,最优决策点变成了 3

所以这时候 3 反超了 1 决策点。所以我们需要告诉计算机什么时候 3 反超一。对于这个题,显然可以 O(1) 计算。然后根据反超时间建一个单调栈 / 单调队列即可。但是有时候这个反超时间要二分。这也就是这个东西叫做二分栈 / 二分队列的原因。

接下来是例题。

P1912 [NOI2009] 诗人小G

fi 代表在第 i 个短句后面打一个回车,前 i 个短句的最小不协调度。然后状态转移方程就是:

fi=fj+cost(j+1,i)

然后发现这符合决策单调性的规律。我们把 cost(l,r) 拆开看看。

cost(l,r)=|(srsl1+rlL)P|

其中 s 表示短句长度的前缀和。

首先我们看函数 f(x)=xP 的二阶导,显然就是 f(x)(2)=P(P1)xP2。当 x>0,P2 的时候,这个东西显然是正的,也就是说函数增长率越来越快。接下来分析 cost(l,r) 函数。可以固定 r 不变,然后看看它关于 l 的变化情况。可以发现,当 r 不变的时候, l 越大,其增长率越慢(因为 l 变小,sl1 也变小,所以相当于 x 变小了)。由于要求最小值,所以决策点应该是单调右移的。

然后套上二分栈二分队列即可。

auto calc = [&](int l, int r) -> long double {
	return dp[l] + qpow((long double)abs(s[r] - s[l] + r - l - 1 - L), P);
};
auto find = [&](int j, int i) -> int { // To cauculate when j becomes better than i
	int l = j, r = n + 1, sum = 0;
	while (l <= r) {
		int mid = l + r >> 1;
		if (calc(j, mid) >= calc(i, mid)) r = mid - 1;
		else l = mid + 1;
	}
	return l;
};

scanf("%d", &T);
while (T -- ) {
	scanf("%d%d%d", &n, &L, &P);
	for (int i = 1; i <= n; i ++ ) {
		scanf("%s", str[i]);
		s[i] = s[i - 1] + strlen(str[i]);
	}
	hh = tt = 1; q[hh] = 0;
	for (int i = 1; i <= n; i ++ ) {
		while (hh < tt and t[hh] <= i) hh ++ ;
		pre[i] = q[hh], dp[i] = calc(q[hh], i);
		while (hh < tt and t[tt - 1] >= find(q[tt], i)) tt -- ;
		t[tt] = find(q[tt], i);
		q[ ++ tt] = i;
	}
} 

2D 决策单调性优化

这种题目通常可以进行分层。比如 fi,j=fi1,k+。这样可以关于 i 进行分层,看做从 i1 层转移到第 i 层。然后观察层之间的转移是否有决策单调性。

通常来说,二维的决策单调性优化使用分治法来解决。具体是这样的:假设 solve(l, r, L, R) 表示当前层处理到了 [l,r] 这个区间,决策点在上一层的 [L,R] 这个区间。然后假设 mid=l+r2。找到转移到 fi,mid 的最优决策点 fi1,MID。然后根据决策单调性可知,能够转移到 fi,[l,mid1] 的最优决策点一定在区间 [L,MID],而 fi,[mid+1,r] 的最优决策点一定在区间 [MID,R] 中(这里假设决策点的递增不是严格的)。递归处理 solve(l, mid - 1, L, MID), solve(mid + 1, r, MID, R) 即可。

可以发现复杂度的瓶颈在于寻找 mid 的最优决策点。如果每次寻找 mid 决策点的复杂度为 Θ(λ),那么时间复杂度就为 O(λknlogn) 了。这里的 k 表示层数。

所以分治法适用于求解 mid 最优转移点时间复杂度为 O(1) 或均摊 O(1) 的情况。

CF833B The Bakery

fi,j 表示将前 i 个数分成 j 段的最大价值。那么那么转移方程就是 fi,j=fk1,j1+cost(k,j)。显然可以根据分的段数分层。然后考虑如何求出 cost(l,r)。考虑使用莫队的思想,开一个桶,然后搞两个指针移动一下。考虑这个复杂度为什么正确。首先在求 mid 的时候如果采取下面的写法:

for (int i = min(R, mid); i >= L; i -- ) {
	int ans = f[i - 1][level - 1] + cost(i, mid);
	if (ans > f[mid][level]) f[mid][level] = ans, MID = i;
}

那么显然指针是单调左移的。
然后接下来 solve(l,MID) 的时候,指针仍然会单调左移。接下来 solve(MID+1,R) 的时候,指针又单调右移。所以指针的移动次数是 O(n) 级别的。也就是做到了均摊 O(1) 求最优决策点。

void solve(int l, int r, int L, int R, int level) {
	if (l > r) return;
	int mid = l + r >> 1, MID = L;
    for (int i = min(R, mid); i >= L; i -- ) {
    	int ans = f[i - 1][level - 1] + cost(i, mid);
    	if (ans > f[mid][level]) f[mid][level] = ans, MID = i;
    }
	solve(l, mid - 1, L, MID, level);
	solve(mid + 1, r, MID, R, level);
}

int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; i ++ )
		scanf("%d", &w[i]);
	memset(f, -0x3f, sizeof f);
	f[0][0] = 0;
	for (int i = 1; i <= k; i ++ )
		solve(1, n, 1, n, i);
	printf("%d\n", f[n][k]);
	return 0;
}
posted @   Link-Cut-Y  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示