一直在你身旁
题面
题解
设 \(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]\)。
想清楚这些后,我们就可以很容易的得到一个状态转移方程。
当我们花费 \(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;
}