一直在你身旁

题面

一直在你身旁

题解

\(dp[l][r]\) 表示在知道答案在 \(l\) ~ \(r\) 范围内之后还需要花费多少才能得出答案。
所以显然的有 \(dp[l][l] = 0, dp[l][l + 1] = a[l], ans = dp[1][n]\)
对于第二个柿子要想明白,因为在我们转移时也要用到这个思路。
就是当我们知道答案在 \(l\) ~ \(l + 1\) 这个范围内之后,根据题意,我们可以花费 \(a[l]\) 的代价知道答案是否大于 \(l\),如果大于,那么答案为 \(l + 1\), 反之答案为 \(l\)
所以花费应该为 \(a[l]\)
想清楚这些后,我们就可以很容易的得到一个状态转移方程。

\[dp[l][r] = min({max_{k = l} ^ {r} {(dp[l][k], dp[k + 1][r])} + a[k]}) \]

当我们花费 \(a[k]\) 之后,我们可以知道答案是在 \(k + 1\) ~ \(r\) 这个区间,还是在 \(l\) ~ \(k\) 这个区间,而在这两个区间中取最大值是为了保证一定能得到答案,考虑的是最坏情况。而在外层取最小值,是在保证了一定能得到答案的情况下,找到最优解。
但这个是 \(n ^ 3\) 显然过不了。
所以我们考虑把中间的 \(max\) 拆开。可以发现,因为长度越长,花费越大,随着 \(k\) 的增加,\(dp[l][k]\) 肯定是单调不降的,而 \(dp[k + 1][r]\) 肯定是单调不升的,所以一定有一个中间点,满足左半边的 \(k\),最大值是 \(dp[k + 1][r]\),右半边的 \(k\) 最大值是 \(dp[l][k]\),对于这个位置我们直接一个 while 就能找到。
然后也是因为长度越长,花费越大,所以当最大值是 \(dp[l][k]\) 时,肯定是 \(k\) 越小,\(dp[l][k]\) 越小, \(a[k]\) 越小,所以 \(dp[l][k] + a[k]\) 越小,所以此时的总的最小值直接取我们找到的那个中间点的位置就好了。
而当最大值是 \(dp[k + 1][r]\) 时,我们直接对这一部分维护一个单调队列就好了。
所以就把它降到了 \(n ^ 2\)
注意 \(l\) 要倒序枚举才能保证当前的 \(l\) ~ \(r\) 的区间内的 \(dp\) 值都是已知的。

代码

#include<cstdio>
#include<iostream>

using namespace std;

int n, t, a[7505], q[10000]; long long dp[7505][7505];

int main() {
	scanf("%d", &t);
	while(t--) {
		scanf("%d", &n);
		for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
		for(int i = 1; i <= n; i++)
			for(int j = 1; j <= n; j++)
				dp[i][j] = 0;
		for(int r = 2, head, tail, k; r <= n; r++) {
			head = tail = 1; q[tail] = k = r;
			for(int l = r - 1; l; l--) {
				if(r - l == 1) {
					dp[l][r] = a[l];
					continue;
				}
				while(k > l && dp[l][k - 1] > dp[k][r]) k--;
				dp[l][r] = dp[l][k] + a[k];
				while(head <= tail && q[head] >= k) head++;
				if(head <= tail) dp[l][r] = min(dp[l][r], dp[q[head] + 1][r] + a[q[head]]);
				while(head <= tail && dp[q[tail] + 1][r] + a[q[tail]] >= dp[l + 1][r] + a[l]) tail--;
				q[++tail] = l; 
			}
		}	
		printf("%lld\n", dp[1][n]);
	}
	return 0;
}
posted @ 2021-07-31 20:14  init-神眷の樱花  阅读(30)  评论(0)    收藏  举报