题解P6563 [SBCOI2020] 一直在你身旁
一道非常值得做的单调队列优化DP,刚开始看到这道题目一直想着二分,愣是没有看出是个DP,后来仔细想了下,可以轻易的写出 \(O(n^3)\) 的暴力.
定义 \(f_{i,j}\) 表示确定区间 \([i,j]\) 内的数字所需的最小花费,得出转移方程
\[f_{i,j}=\min(\max_{l\le k < r}(f_{l,k},f_{k+1,r})+a_k)
\]
显然是个区间DP
通过标签观察式子有点类似 \(f_k+a_k\) 的形式,似乎可以优化,首先不好直接对这个式子下手,可以分类讨论一下,把其中的 \(\max\) 去掉
仔细思考一下,可以发现 \(f_{i,j}\le f_{i,j+1}, f_{i,j} \ge f_{i+1,j}\)
那么也就是说,我们在分类讨论的时候只需要找到一个最小的 \(x\) 使得 \(f_{l,x}>f_{x+1,r}\) 就行了
对于 \(f_{l,k}>f_{k+1,r}\) 的情况
\(f_{l,r}=\min\limits_{l\le k < r} f_{l,k}+a_k\),其中 \(f_{l,k}+a_k\) 是单调不减的,所以答案直接为 \(f_{l,x}+a_x\)
对于 \(f_{l,k}<f_{k+1,r}\) 的情况就稍微比较麻烦了
\(f_{l,r}=\min\limits_{l\le k < r}(f_{k+1,r}+a_k)\),每次用单调队列维护一个最小值就行了.
所以时间复杂度就可以优化成 \(O(n^2)\)
代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 7105;
typedef long long ll;
ll f[N][N];
int a[N], q[N];
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
f[i][i] = 0, f[i][i + 1] = a[i];
for (int r = 2; r <= n; r++)
{
int front = 0, tail = -1, p = r - 1;
q[0] = r - 1;
for (int l = r - 2; l >= 1; l--)
{
while (p >= l && f[l][p] > f[p + 1][r])//找到最小的x
p--;
f[l][r] = f[l][p + 1] + a[p + 1];//这里主要需要加一
if (front <= tail && q[front] > p)//第二种情况
front++;
if (front <= tail)
f[l][r] = min(f[l][r], f[q[front] + 1][r] + a[q[front]]);
while (front <= tail && f[q[tail] + 1][r] + a[q[tail]] >= f[l + 1][r] + a[l])
tail--;
q[++tail] = l;
}
}
printf("%lld\n", f[1][n]);
}
return 0;
}