牛客331194I多校 - The Great Wall II
题意
- 给出一个长度为 n(n≤8000) 的序列,你需要将序列切割成 k 段,每段对答案的贡献为这一段的最大值。
- 需要最小化答案。
- 你需要对于所有的 k∈[1,n],回答每个询问。
思路
初步思路
- 考虑 DP。
- 令 dp[i][j] 为前 i 个物品,分成 j 段,的最小值。
- 转移:dp[i][j]←min(dp[x][j−1])+max(ax…ai)
- 复杂度:O(n3)。
优化
- 观察转移方程:dp[i][j]←min(dp[x][j−1])+max(ax…ai)
- 我们是否可以对于每个 dp[i][j],使用单调栈快速地求出 min(dp[x][j−1]) 实现 O(n2) 的转移?
- 不可以。dp[x][j−1] 的确是最小的,但是加上后面的那个最大值,就不一定是最优的了。
证明:这里我们分析区间 [x,i]。随着 x 指针的向左移动,min(dp[x][j−1]) 单调递减,但是 max(ax…ai) 单调递增。
- 我们能观察到,在上面的转移方程中,随着 x 指针的向左移动,min(dp[x][j−1]) 单调递减,但是 max(ax…ai) 单调递增。
- 所以对于一个 ai而言,在序列中存在一段区间 ap…ai,满足 max(ap…ai)=ai
- 这段区间之前的部分,也就是 a1…ap−1,满足 max(ap…ai)>ai
- 转移:
- 对于 j∈[p,i−1],tmp1=min(dp[p…i−1])+ai
- 对于 j∈[1,p−1],tmp2=min(dp[1⋯−p−1])
- dp[i]=min(tmp1,tmp2)
- 以上转移可以用单调栈维护。
- 既然要找分界点 p,那么单调栈一定要维护当前 ai 的值,而且这个单调栈是由 ai 的单调递减的单调栈。
- 显然,还要维护当前的 dp 值 cur_dp,和之前的最小的 dp 值 pre_dp。
- 转移:
- 弹栈条件:stk.top.a<ai,弹栈的同时 min_dp=min(stk.top.cur_dp+ai)
- 弹栈停止意味着找到了分界点。此时在这之前意味着满足 max(ap…ai)>ai 的条件,那么 min_dp=min(min_dp,stk.top.pre_dp)
- 当前的 dp 值就是 min_dp。
- 入栈:cur_dp=min_dp,predp=min(stk.top.pre_dp,cur_dp)
代码
#include <cstdio>
#include <iostream>
#include <stack>
#define int long long
const int N = 8010;
const int INF = 1e9;
using namespace std;
struct STK{int ai,dp,val;};
stack<STK> stk;
int dp[N][N], ai[N];
int n;
void Sol()
{
for (int i=0; i<=n; i++)
{
for (int j=0; j<=n; j++)
{
dp[i][j] = INF;
}
}
dp[0][0] = 0;
for (int j=1; j<=n; j++)
{
while (!stk.empty())
stk.pop();
for (int i=j; i<=n; i++)
{
int min_dp = dp[i-1][j-1];
while (!stk.empty() && stk.top().ai <= ai[i])
{
min_dp = min(min_dp, stk.top().dp);
stk.pop();
}
int min_val = INF;
if(!stk.empty())
min_val = stk.top().val;
dp[i][j] = min(min_val, min_dp+ai[i]);
stk.push({ai[i], min_dp, dp[i][j]});
}
}
for (int i=1; i<=n; i++)
{
printf("%lld\n",dp[n][i]);
}
}
signed main()
{
scanf("%lld",&n);
for (int i=1; i<=n; i++)
{
scanf("%lld",&ai[i]);
}
Sol();
return 0;
}