区间 DP 选做

Heldivis

2024-11-04 16:13:58

Algo. & Theory

区间 DP

by Heldivis & Redamancy_Lydic & zhujiangyuan

A - CF1509C The Sports Festival

发现 d_i 一定是单调不降的,所以我们尽可能保证每一步都最小。

a_i 排序,如果当前选出了 i 个数,一定是在排过序后的数组中连续的一段。

dp_{l,r} 表示选出的区间左端点为 l,右端点为 r 时的答案。则最终答案为 dp_{1,n}

对于 1\le i\le n,钦定 i 为选出的第一项,则 dp_{i,i}=0。转移:dp_{l,r}=\min(dp_{l+1,r},dp_{l,r-1})+a_r-a_l

这样做复杂度是 O(n^3) 的。

发现可以从外向内转移,也就是倒着选,dp_{l,r} 为当前选的数 \in[1,l]\cap [r,n] 时的答案。初始:dp_{1,n} = a_n - a_1

转移:dp_{l,r}=\min(dp_{l-1,r},dp_{l,r+1})+a_r-a_l

答案:\min\limits_{i=1}^ndp_{i,i}

sort (a + 1, a + n + 1);
memset (dp, 0x3f, sizeof (dp));
dp[1][n] = a[n] - a[1];
for (int i = 1; i <= n; i++)
  for (int j = n; j >= i; j--)
    if (i != 1 || j != n) dp[i][j] = min (dp[i][j + 1], dp[i - 1][j]) + a[j] - a[i];
for (int i = 1; i <= n; i++) ans = min (ans, dp[i][i]);
cout << ans;

B - CF1728D Letter Picking

<h4>区间 DP - 平方复杂度</h4>

考虑一个区间 l\sim r

int Fn(int l, int r) {
  if (l > r) return 0;
  if (mp.count({l, r})) return mp[{l, r}];
  int ll = Fn(l + 2, r), lr = Fn(l + 1, r - 1), rr = Fn(l, r - 2);
  if ((ll == 1 || (ll == 0 && s[l] < s[l + 1])) && (lr == 1 || (lr == 0 && s[l] < s[r]))) 
    return mp[{l, r}] = 1;  // 先手选 l, 后手必败
  if ((rr == 1 || (rr == 0 && s[r] < s[r - 1])) && (lr == 1 || (lr == 0 && s[r] < s[l]))) 
    return mp[{l, r}] = 1;  // 先手选 r, 后手必败
  if ((ll == 1 || (ll == 0 && s[l] <= s[l + 1])) && (lr == 1 || (lr == 0 && s[l] <= s[r]))) 
    return mp[{l, r}] = 0;  // 先手选 l, 后手必不赢
  if ((rr == 1 || (rr == 0 && s[r] <= s[r - 1])) && (lr == 1 || (lr == 0 && s[r] <= s[l]))) 
    return mp[{l, r}] = 0;  // 先手选 r, 后手必不赢
  return mp[{l, r}] = -1;
}

<h4>推性质 - 线性复杂度</h4>

发现先手必不败。因为只剩最后两个时,只要先取走不大的一个,那么要么平局,要么先手赢。

然后发现,回文、两两重复或回文套两两重复时平局,其余先手胜。

scanf("%s", s + 1), l = 1, r = strlen(s + 1);
while (l < r && s[l] == s[r]) ++l, --r;
if (l >= r) return puts("Draw"), void();
for (int i = l; i + 1 <= r; i += 2) if (s[i] != s[i + 1]) return puts("Alice"), void();
puts("Draw");

C - CF1666J Job Lookup

其任意一个节点的左子树内所有节点编号都小于它,右子树内所有节点编号都大于它。所以一个区间是一个完整的子树。

考虑枚举一个区间的根 k,每次转移把路径的贡献拆开,加上左子树到非左子树的贡献、右子树到非右子树的贡献,这两部分在 c 矩阵上是一个矩形,前缀和维护即可。方案在转移时记录区间的根,最后递归求解即可。

转移方程:

f(l,r) \gets \min_{k=l}^{r}\{f(l,k-1) + f(k+1,r) + \operatorname{Calc}(l, k - 1, 1, l - 1) + \operatorname{Calc}(l, k - 1, k, n) + \operatorname{Calc}(k + 1, r, 1, k) + \operatorname{Calc}(k + 1, r, r + 1, n)\} \\ \operatorname{Calc}(l,r,x,y)=\sum_{i=l}^r\sum_{j=x}^yc_{i,j}
ll Calc(int sx, int fx, int sy, int fy) {
  if (sx > fx || sy > fy) return 0;
  return s[fx][fy] - s[fx][sy - 1] - s[sx - 1][fy] + s[sx - 1][sy - 1];
}
void Dfs(int l, int r, int p) {
  if (l > r) return void();
  if (l == r) return ans[l] = p, void();
  ans[fa[l][r]] = p;
  Dfs(l, fa[l][r] - 1, fa[l][r]), Dfs(fa[l][r] + 1, r, fa[l][r]);
}

for (int len = 2; len <= n; ++len) {
  for (int l = 1, r; (r = l + len - 1) <= n; ++l) {
    for (int k = l; k <= r; ++k) {
      ll tmp = (k != l) * f[l][k - 1] + (k != r) * f[k + 1][r] +
               Calc(l, k - 1, 1, l - 1) + Calc(l, k - 1, k, n) + Calc(k + 1, r, 1, k) + Calc(k + 1, r, r + 1, n);
      if (f[l][r] > tmp) f[l][r] = tmp, fa[l][r] = k;
    }
  }
}
Dfs(1, n, 0);

D - [USACO04OPEN] Turning in Homework G

因为交作业不需要时间,所以交一个区间的作业的时候,一定是先交左端点或者先交右端点。

dp_{l,r,0/1} 为还有 [l,r] 的作业没交,当前在 l/r 时的答案。

初始:dp_{1,n,0}=\max(x_1,t_1)dp_{1,n,1}=\max(x_n,t_n)

转移:从相邻区间的两个状态转移而来。

dp_{i,j,0}=\max\{t_i,\min\{dp_{i-1,j,0}+x_i-x_{i-1},dp_{i,j+1,1}+x_{j+1}-x_i\}\} dp_{i,j,1}=\max\{t_j,\min\{dp_{i-1,j,0}+x_j-x_{i-1},dp_{i,j+1,1}+x_{j+1}-x_j\}\}

答案为 \min\limits_{i=1}^n\{|b-x_i|+\min\{dp_{i,i,0},dp_{i,i,1}\}\}

时间复杂度 \mathcal O(n^2)

sort (a + 1, a + n + 1, [] (Node a, Node b) { return a.x < b.x; });
memset (dp, 0x3f, sizeof (dp));
dp[1][n][0] = max (a[1].x, a[1].t);
dp[1][n][1] = max (a[n].x, a[n].t);
for (int i = 1; i <= n; ++i)
  for (int j = n; j >= i; j--) {
    if (i == 1 && j == n) continue;
    dp[i][j][0] = max (a[i].t, min (dp[i - 1][j][0] + a[i].x - a[i - 1].x, dp[i][j + 1][1] + a[j + 1].x - a[i].x));
    dp[i][j][1] = max (a[j].t, min (dp[i - 1][j][0] + a[j].x - a[i - 1].x, dp[i][j + 1][1] + a[j + 1].x - a[j].x));
  }
for (int i = 1; i <= n; i++) ans = min (ans, min (dp[i][i][0], dp[i][i][1]) + abs (b - a[i].x));
cout << ans;

E - [CERC2014] Outer space invaders

考虑如果对一个人进行 DP,发现由于左右端点不固定,不很好转移,考虑对时间 DP。时间显然可以离散化到 O(n)

f(l,r) 表示消灭出现时间被 l\sim r 时间完全包含 的敌人的最小代价,考虑转移:

枚举对距离最大的敌人的攻击时间 k,此时所有时间区间包含 k 的敌人都消灭了,还剩下 完全被 l\sim k-1k + 1\sim r 时间完全包含的敌人。由 DP 的含义得到就是 f(l, k - 1) + f(k + 1, r)

p 为区间内距离最大的敌人,则转移式:

f(l,r)\gets\max_{k=l_p}^{r_p}\{f(l,k-1)+f(k + 1, r) + d_p\}

如果区间不完全包含某个敌人 DP 值为 0;答案为消灭所有敌人,即被整个时间包含的敌人 f(1,w)w 时总时间。

for (int len = 1; len <= w; ++len) {
  for (int l = 1, r, p = 0; (r = l + len - 1) <= w; ++l) {
    for (int i = 1; i <= n; ++i)
      if (l <= a[i].l && a[i].r <= r && (a[i].d > a[p].d)) p = i;
    if (!p) {
      f[l][r] = 0;
    } else {
      f[l][r] = 1E18;
      for (int k = a[p].l; k <= a[p].r; ++k) Chmin(f[l][r], f[l][k - 1] + f[k + 1][r]);
      f[l][r] += a[p].d;
    }
    p = 0;  // 记得清空 QWQ
  }
}
printf("%lld\n", f[1][w]);

F - [USACO17JAN] Subsequence Reversal P

根据数据范围,不难想到 DP 状态应该是 n^4 级别的。

先考虑当没有反转区间的操作时如何转移。

dp_{l,r,L,R} 表示当前区间为 l\sim r,值域 \in [L,R] 时的答案。转移时枚举四个维度,可以从 dp_{l,r,L,R-1},dp_{l,r,L+1,R},dp_{l+1,r,L,R},dp_{l,r-1,L,R} 转移过来。

加上翻转操作后,我们思考其本质。翻转一个子序列可以理解为交换某几对数字的位置,这样的话相当于如果 a_l=R 或者 a_r=L 的话,我们可以通过翻转 l\sim r 中的任意一个包含 l,r 的子序列来满足条件,即 dp_{l,r,L,R}=dp_{l+1,r-1,L,R}+[a_l=R]+[a_r=L]

由于区间 DP 按照区间从小到大的顺序,故可以保证这样的翻转满足题目条件,所以这道题就结束了。

cin >> n;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++)
  for (int l = 1; l <= a[i]; l++)
    for (int r = a[i]; r <= 50; r++) dp[i][i][l][r] = 1;
for (int len = 2; len <= n; len++) {
  for (int l = 1; l <= n - len + 1; l++) {
    int r = l + len - 1;
    for (int lenn = 1; lenn <= 50; lenn++) {
      for (int L = 1; L <= 50 - lenn + 1; L++) {
        int R = L + lenn - 1;
        dp[l][r][L][R] = max(dp[l][r][L + 1][R], dp[l][r][L][R - 1]);
        dp[l][r][L][R] = max(dp[l][r][L][R], dp[l + 1][r][L][R] + (a[l] == L));
        dp[l][r][L][R] = max(dp[l][r][L][R], dp[l][r - 1][L][R] + (a[r] == R));
        dp[l][r][L][R] = max(dp[l][r][L][R], dp[l + 1][r - 1][L][R] + (a[l] == R) + (a[r] == L));
      }
    }
  }
}
cout << dp[1][n][1][50];

G - [春季测试 2023] 圣诞树

考虑对于:

A  B
C  D

这样的节点,一定不会是 ADBC 这样的路径,因为 AD\gt CD,BC\gt AB,那么 AD+DB+BC\gt CD+DB+BA,不如 ABDC 更优,即线不会有交叉。

考虑设 f(l,r,0/1) 表示从区间 l\sim r 的左 / 右端点,走完区间内的最小代价。把原序列从 k 断开,改为数值上形如 \lor 的一个长为 n-1 的序列。则一定是左边选若干个、然后右边选若干个这样,单独看两边,每次选择时高度单调不升。

答案为 \min\{f(1,n-1,0)+\operatorname{dis}(1,n),f(1,n-1,1)+\operatorname{dis}(n-1,n)\},其中 \operatorname{dis}(i,j) 表示新序列上 i,j 点之间的距离。特别地,方便起见,令原序列上的 k 在新序列的第 n 位上,故 DP 过程仅为前 n-1 位的决策,与第 n 位无关。

转移式子:

f(l,r,0) = \min{\large\{}f(l + 1,r,0) + \operatorname{dis}(l, l + 1), f(l + 1,r,1) + \operatorname{dis}(r, l)\large\}\\ f(l,r,1) = \min{\large\{}f(l,r - 1,0) + \operatorname{dis}(l, r),f(l,r - 1,1) + \operatorname{dis}(r - 1, r)\large\}

输出方案可以在记录一个 p(l,r,0/1)=0/1 表示在区间 l\sim r 的左 / 右时,上一次是从左 / 右转移来,递归输出方案。

inline double Dis(int i, int j) {
  return sqrt((a[i].x - a[j].x) * (a[i].x - a[j].x) + (a[i].y - a[j].y) * (a[i].y - a[j].y));
}

void Dfs(int l, int r, int op) {
  if (l == r) return printf(" %d", a[l].id), void();
  if (op) printf(" %d", a[r].id), Dfs(l, r - 1, p[l][r][op]);
  else printf(" %d", a[l].id), Dfs(l + 1, r, p[l][r][op]);
}

int main() {
  cin >> n;
  for (int i = 1; i <= n; ++i) cin >> s[i].x >> s[i].y, s[i].id = i, ((s[i].y > maxy) && (maxy = s[i].y, k = i));
  for (int i = 1; i <= n; ++i) a[(i - k + n) % n] = s[i]; a[n] = s[k];
  for (int len = 2; len < n; ++len)
    for (int l = 1, r; (r = l + len - 1) < n; ++l) {
      f[l][r][0] = f[l][r][1] = 1E100;
      if (f[l][r][0] > f[l + 1][r][0] + Dis(l + 1, l)) f[l][r][0] = f[l + 1][r][0] + Dis(l, l + 1), p[l][r][0] = 0;
      if (f[l][r][0] > f[l + 1][r][1] + Dis(r, l)) f[l][r][0] = f[l + 1][r][1] + Dis(r, l), p[l][r][0] = 1;
      if (f[l][r][1] > f[l][r - 1][0] + Dis(l, r)) f[l][r][1] = f[l][r - 1][0] + Dis(l, r), p[l][r][1] = 0;
      if (f[l][r][1] > f[l][r - 1][1] + Dis(r - 1, r)) f[l][r][1] = f[l][r - 1][1] + Dis(r - 1, r), p[l][r][1] = 1;
    }
  cerr << min(f[1][n - 1][0] + Dis(1, n), f[1][n - 1][1] + Dis(n - 1, n)) << endl;
  printf("%d", k), f[1][n - 1][0] + Dis(1, n) < f[1][n - 1][1] + Dis(n - 1, n) ? Dfs(1, n - 1, 0) : Dfs(1, n - 1, 1);
  return 0;
}

H - 括号序列再战猪猪侠

根据题目要求,容易想到区间计数动态规划。

我们只考虑左括号,设 dp_{l,r} 表示当前考虑从第 l 到第 r 个左括号时的方案数,注意,这里的状态同时包含了这些左括号对应的右括号。

考虑转移,我们设 A 为一个合法括号串,它表示的区间是 [l,r],可以很自然的分三种情况:

对于这些限制的快速判断我们可以维护一个矩阵,每条信息相当于矩阵上的一个点,这样对于前文的要求可以快速用二位前缀和的值的存在与否判断是否全部满足。

int ask(int l1, int r1, int l2, int r2) {
  return sum[l2][r2] - sum[l1 - 1][r2] - sum[l2][r1 - 1] + sum[l1 - 1][r1 - 1];
}
void Main() {
  n = read(), m = read(), ff = true;
  for (int i = 0; i <= n; i++)
    for (int j = 0; j <= n; j++) a[i][j] = sum[i][j] = dp[i][j] = 0;
  for (int i = 1; i <= m; i++) {
    int x = read(), y = read();
    a[x][y] = 1;
    if (x == y) ff = 0;
  }
  if (!ff) return puts("0");, void();

  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
      sum[i][j] = a[i][j] + sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
  for (int i = 1; i <= n; i++) dp[i][i] = 1;
  for (int len = 2; len <= n; len++)
    for (int l = 1; l <= n - len + 1; l++) {
      int r = l + len - 1;
      if (!ask(l, l + 1, l, r)) (dp[l][r] += dp[l + 1][r]) %= mod;
      if (!ask(l + 1, l, r, l)) (dp[l][r] += dp[l + 1][r]) %= mod;
      for (int k = l + 1; k < r; k++)
        if (!ask(l, l + 1, l, k) && !ask(k + 1, l, r, k)) 
          dp[l][r] = (dp[l][r] + dp[l + 1][k] * dp[k + 1][r] % mod) % mod;
    }
  printf("%lld\n", dp[1][n]);
}

I - [THUSC2016] 成绩单

当你列出 DP 状态发现无法转移的时候,不妨添加状态。

f_{i,j,l,r} 为要消除 [i,j] 区间,且当前未消除的数值域均在 [l,r] 时的最小代价。

dp_{i,j} 为消除 [i,j] 区间的最小代价。

dp_{i,j}=\min f_{i,j,l,r}+a+b\times (r-l)^2

转移:

  1. 枚举断点,合并两个区间。f_{i,j,l,r}=\min\limits_{k=i}^{j-1} f_{i,k,l,r}+f_{k+1,j,l,r}

  2. 找到 [i,j] 区间中最靠左的值域不在 [l,r] 的数位置,记为 p,找到最靠右的值域不在 [l,r] 的数的位置,记为 q。则 f_{i,j,l,r}=dp_{p,q}

枚举 i,j,l,r 时间复杂度 \mathcal O(n^4),枚举断点 \mathcal O(n),总时间复杂度 \mathcal O(n^5)

for (int i = 1; i <= n; i++) {
  dp[i][i] = A;
  for (int l = 1; l <= m; ++l)
    for (int r = l; r <= m; ++r)
      if (a[i] < l || a[i] > r) f[i][i][l][r] = A;
      else f[i][i][l][r] = 0;
}
for (int len = 1; len < n; len++)
  for (int i = 1, j = i + len; j <= n; ++i, ++j)
    for (int l = 1; l <= m; ++l)
      for (int r = l; r <= m; ++r) {
        int p = 0, q = 0;
        for (int k = i; k <= j; ++k) if (a[k] < l || a[k] > r) { p = k; break; }
        for (int k = j; k >= i; --k) if (a[k] < l || a[k] > r) { q = k; break; }
        if (p > 0 && q > 0) f[i][j][l][r] = dp[p][q];
        for (int k = i; k < j; ++k)
          f[i][j][l][r] = min(f[i][j][l][r], f[i][k][l][r] + f[k + 1][j][l][r]);
        dp[i][j] = min(dp[i][j], f[i][j][l][r] + A + B * (tmp[r] - tmp[l]) * (tmp[r] - tmp[l]));
      }
cout << dp[1][n];

J - [CQOI2007] 涂色

考虑一个结论:一定存在一种最优方案使得使得任意一次染色的区间一定是完全包含之前某一次染色区间或者与之前某一次染色区间完全不交且不与之前所有染色区间相交。

简单证明,如果我们当前的染色方案与之前某一次相交,那么我们完全可以缩短当前染色区间使得不交。

我们发现这个性质能够把染色的过程抽象为类似线段树的结构,即线段与线段之间只存在包含和并列的关系,一个大区间的答案可以由其中的小区间合并而来,于是我们可以想到用区间 DP 解决这个问题。

这样我们设 dp_{l,r} 表示染色区间为 l,r 时的答案,对上述两种情况分别考虑:

scanf("%s", s + 1), n = strlen(s + 1), memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++) {
  for (int l = 1; l <= n - len + 1; l++) {
    int r = l + len - 1;
    if (s[l] == s[r])  // 此时根据定义应有 dp[l][r] = dp[l][r - 1] = dp[l + 1][r],所以可以直接转移
      dp[l][r] = min(dp[l][r - 1], dp[l + 1][r]);
    else
      for (int k = l; k <= r; k++) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
  }
}
cout << dp[1][n];

K - [ZJOI2016] 线段树

首先,题目上说让期望乘上 (\frac{n(n+1)}{2})^q 的目的就是让我们求方案数与值的乘积。

然后我们考虑在操作过后一个位置上的值相对于原来的值肯定是不降的,于是可以想到对每一个值 v ,原序列中所有 \le v 的元素一定构成了若干连续的区间。对每一个这样的区间而言,操作过后这个区间的左右端点一定是向中间移动的。

于是我们可以对每个区间都考虑一个 DP。设 dp_{v,x,l,r} 表示操作 x 次后区间 [l,r] 中的数字均 \le v,且第 l-1r+1 个元素都 \gt v 的方案数。

转移的话显然可以从 dp_{v,x-1,l,r},dp_{v,x-1,i,r},dp_{v,x-1,l,i} 转移过来。具体的,第一种转移相当于第 x 次操作对于区间 [l,r] 来说应该是无用的,即操作区间 [1,l-1],[l,r],[r+1,n] 的总方案数。第二种和第三种转移类似,以第二种为例,相当于第 x 次操作应当在区间 [1\sim i-1,l-1] 进行,这个可以使用两个前缀和优化。转移方程就不写了,其中 x 这一维显然可以滚掉。

回到题面,我们现在要解决的问题就是求每个数字最终大小为某个定值时的方案数。上述 DP 求的是每个区间最终值不大于某个定值时的方案数,那我们对于一个区间 [l,r] 而言,若用 f(j) 表示 dp_{j,q,l,r},那么这个区间最终值等于 j 的方案数显然就是 f(j)-f(j-1)

这样对于每一个元素,我们只需要找到所有包含它的区间,再对于这个元素在该区间内可能变成的所有值都累加答案即可。根据定义,可以保证方案不会重复。

根据数据随机的条件,时间复杂度近似 \mathcal{O}(n^2q),可以通过本题。

但是还有更优的做法。我们发现,对于每一个值 v,它所带来的系数是固定的,所以我们可以对 v 这一维直接乘上对应的系数后求和,把总维度优化到三个,用数组 dp'_{x,l,r} 直接表示操作 x 次后区间 [l,r] 的答案,这样我们可以发现转移与上文所述完全一致,不同点在于初值。

具体的,在赋初值的时候我们枚举区间 [l,r],设 ma 为初始序列中下标为 l-1r+1 元素的最小值,同时设 num 表示区间 [l,r] 中元素的最大值,那么可以推出 dp'_{0,l,r}=num-ma

inline int calc(int l, int r) {
  return (l * (l - 1) / 2 % mod + (r - l + 1) * (r - l + 2) / 2 % mod + (n - r) * (n - r + 1) / 2 % mod) % mod;
}
int main() {
  cin >> n >> q;
  for (int i = 1; i <= n; i++) a[i] = read();
  for (int l = 1; l <= n; l++) {
    int ma = 0;
    for (int r = l; r <= n; r++) {
      ma = max(ma, a[r]);
      if (ma >= min(a[l - 1], a[r + 1])) continue;
      int num = min(a[l - 1], a[r + 1]);
      if (l == 1 && r == n) num = 0;
      dp[s1][l][r] = (ma - num + mod) % mod;
    }
  }
  for (int l = 1; l <= n; l++) {  
    for (int r = l; r <= n; r++)
      sum[s1][l][r] = (sum[s1][l - 1][r] + dp[s1][l][r] * (l - 1) % mod) % mod;
    for (int r = n; r >= l; r--)
      sumr[s1][l][r] = (sumr[s1][l][r + 1] + dp[s1][l][r] * (n - r) % mod) % mod;
  }
  for (int T = 1; T <= q; T++) {
    swap(s1, s2);
    for (int len = 1; len <= n; len++)
      for (int l = 1, r; (r = l + len - 1) <= n; l++)
        dp[s1][l][r] = ((dp[s2][l][r] * calc(l, r) % mod + sum[s2][l - 1][r]) % mod + sumr[s2][l][r + 1]) % mod;
    for (int l = 1; l <= n; l++) {
      for (int r = l; r <= n; r++)
        sum[s1][l][r] = (sum[s1][l - 1][r] + dp[s1][l][r] * (l - 1) % mod) % mod;
      for (int r = n; r >= l; r--)
        sumr[s1][l][r] = (sumr[s1][l][r + 1] + dp[s1][l][r] * (n - r) % mod) % mod;
    }
  }
  for (int i = 1; i <= n; i++) {
    int ans = 0;
    for (int l = 1; l <= i; l++)
      for (int r = i; r <= n; r++) ans = (ans + dp[s1][l][r]) % mod;
    printf("%d ", ans);
  }
  return 0;
}

L - CF1987F Interesting Problem

考察一个数 a_i 能被删去的条件:

  1. 由于每次删除的性质,下标只会越来越小,并且奇偶性不变。所以必须满足 (a_i\le i)\land(a_i\equiv i\pmod2)
  2. 可以在 [1,r) 的范围内恰好进行 \dfrac{i-a_i}{2} 次操作使得 a_ii 相等。

条件 1. 是好维护的,考虑针对条件 2. 进行 DP:设 f(l,r) 表示删空 l\sim r 所需要的最少操作前缀 [1,l) 次数,转移如下:

  1. a_la_r 同时消去 那么需要进行完删空 l+1\sim r-1 的操作后,仍满足条件 1. 使得可以对 a_l 操作,即 f(l+1,r-1)\le\dfrac{l-a_l}{2}(或区间仅有两个数):

    f(l,r) \xleftarrow[\large f(l+1,r-1)\le\frac{l-a_l}{2}\lor r-l=1]{\min} \dfrac{l - a_l}{2}
  2. 枚举断点 k 那么需要进行若干次操作,使得 a_l=l,同时至少要满足左半部分清空的 f(l,k-1) 的代价,以及右半部分 f(k+1,r)-\dfrac{k-l+1}{2} 的代价,减去是清空左半部分时的贡献。也就是:

    {f(l,r) \xleftarrow{\min} \max\left\{\dfrac{l - a_l}{2},f(l,k)+f(k+1,r)-\dfrac{k-l+1}{2}\right\}}

然后发现你会 \texttt{\color{#66AAFF}Wrong answer on test 20}

因为你状态设的是设 f(l,r) 表示删空 l\sim r 所需要的最少操作前缀 [1,l) 次数,所以当左半部分清空之后,右半部分 f(k+1,r)-\dfrac{k-l+1}{2} 的代价是独立计算的。所以第一个转移为:

f(l,r) \xleftarrow{\min} \max\left\{\dfrac{l - a_l}{2},f(l,k),f(k+1,r)-\dfrac{k-l+1}{2}\right\}

然后你就得到了 f(l,r),然后你发现答案不是 f(1,n)废话

考虑 f(l,r) 的含义是删空区间 l\sim r 的一个限制,删空区间 l \sim r 的价值是 \dfrac{r-l+1}{2},所以考虑通过这个限制 & 贡献再跑一次 DP。

dp_i 表示前 i 个数的最优解,那么有转移:

dp_i=\max_{2|(i-j+1)}\left\{dp_{j-1} + \dfrac{i-j+1}{2}\right\}\quad

最后答案就是 dp_n 了。

代码就是模拟以上 DP,注意区间长度恒为偶数,记得赋初值。

n = read();  for (int i = 1; i <= n; ++i) a[i] = read();
for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) f[i][j] = 1E9;
for (int len = 2; len <= n; len += 2)
  for (int l = 1, r; (r = l + len - 1) <= n; ++l)  {
    if (((l & 1) != (a[l] & 1)) || (l < a[l])) continue;
    if (len == 2 || f[l + 1][r - 1] <= (l - a[l]) / 2)
      f[l][r] = (l - a[l]) / 2;
    for (int k = l + 1; k <= r - 1; k += 2)
      Chmin(f[l][r], max({(l - a[l]) / 2, f[l][k], f[k + 1][r] - (k - l + 1) / 2}));
    }
for (int i = 1; i <= n; ++i) {
  dp[i] = dp[i - 1];
  for (int j = i - 1; j > 0; j -= 2) 
    if (f[j][i] <= dp[j - 1]) Chmax(dp[i], dp[j - 1] + (i - j + 1) / 2);
}
printf("%d\n", dp[n]);

M - [THUPC2021] 小 E 爱消除

<h4>简要题意</h4>

从一个序列两端取数放入栈中,栈中有两个数相同即可消去。求最后栈内元素最少值,和在此条件下栈最大空间最小值。

<h4>转移 1</h4>

f(l,r)=(x,y) 表示区间 l\sim r 中,最少剩 x 个,此时最小空间为 y

考虑转移:

  1. 不操作,直接将 a_l/a_r 放入栈中:
f(l,r)\xleftarrow{\min}\min\{f(l + 1, r), f(l, r - 1)\}+ (1, 1)
  1. 使 a_l 和某一个数匹配:

    考虑枚举和它匹配的 i 使得 a_i = a_l,那么就需要两个数同时再序列两端(a_l,\dots,a_i),或同时在序列同一端(a_l,a_i,\dots)。

    • 变换至序列两端:

      那么就需要取出 [i+1,r] 的数字,同时有可能去除 [l+1,j]j\lt i)的一段数字去和 [i+1,r] 匹配消去。

      g(l_1,r_1,l_2,r_2) 表示删去 [l_1,r_1]\cap[l_2,r_2] 所需要的最大空间,则有转移:

      f(l,r)\xleftarrow{\min}\min{\LARGE\{}{\Large(}f(j+1,i-1)\mathrm{.first},\max{\large\{}2,f(j+1,i-1)\mathrm{.second},g(l+1,j,i+1,r)+1{\large\}}{\Large)}{\LARGE\}}
    • 变换至序列一端:

      同理需要取出 [l+1,i-1] 的数字,同时可能去除 [j,r]j\gt i)的一段数字和 [l+1,i-1] 匹配消去。

      转移:

      f(l,r)\xleftarrow{\min}\min{\LARGE\{}{\Large(}f(i + 1,j - 1)\mathrm{.first},\max{\large\{}2,f(i+1,j-1)\mathrm{.second},g(l+1,i-1,j,r)+1{\large\}}{\Large)}{\LARGE\}}

      以上两种在代码中实现是可以令 j= j+1 就不用分讨了。

  2. 使 a_r 和某一个数匹配:

    同理分为变换至序列两端、变换至序列一端两类,转移类似。

<h4>转移 2</h4>

然后中间有个 g(l_1,r_1,l_2,r_2),考虑它的转移:

  1. 使 a_{l_1} 和某一个匹配:

    • 枚举 i \in[l_1,r_1]a_i = a_{l_1}

      找一个 j\in[l_2-1,r_2]j=l_2-1 相当于不选另一个区间),有转移:

      g(l_1,r_1,l_2,r_2)\xleftarrow{\min}\max(g(i + 1, r_1, l_2, j), g(l1 + 1, i - 1, j + 1, r_2) + 1)

      即:消去内部的空间,消去外部的和这次空间,两者最大值。

    • 枚举 i \in[l_2,r_2]a_i = a_{l_1}

  2. 使 a_{r_2} 和某一个匹配

    • 枚举 i \in[l_1,r_1]a_i = a_{r_2}
    • 枚举 i \in[l_2,r_2]a_i = a_{r_2}

其实 f,g 的转移都是相似的,所以就做完了。

<h4>时间复杂度</h4>

$f$ 的状态是 $O(n^4)$ 的,转移是 $O(n^2)$ 的,一共 $O(n^6)$。$50^6\approx1.5\times10^{10}‬$,但是枚举 $i,j$ 的时候有范围限制,跑出来转移次数在 $10^9$ 左右,肯定还跑不满,卡卡常就过了。 <h4>代码</h4> 细节比较多: - DP 过程可以记忆化,~~记忆化没记忆化上结果瞎卡了十分钟常 TvT~~。 - `l1` `r1` `r2` `l2` 会很乱。~~写错一个调了二十分钟 TvT~~。 ```cpp using pii = pair<int, int>; int G(int l1, int r1, int l2, int r2) { if (l1 > r1 && l2 > r2) return 0; if (g[l1][r1][l2][r2]) return g[l1][r1][l2][r2]; int res = Inf; for (int i = l1; i <= r1; ++i) for (int j = l2 - 1; j <= r2; ++j) { if (i != l1 && a[i] == a[l1]) Chmin(res, max({G(i + 1, r1, l2, j), G(l1 + 1, i - 1, j + 1, r2) + 1})); if (j != r2 && a[i] == a[r2]) Chmin(res, max({G(i + 1, r1, l2, j), G(l1, i - 1, j + 1, r2 - 1) + 1})); } for (int i = l2; i <= r2; ++i) for (int j = r1 + 1; j >= l1; --j) { if (i != r2 && a[i] == a[r2]) Chmin(res, max({G(j, r1, l2, i - 1), G(l1, j - 1, i + 1, r2 - 1) + 1})); if (j != l1 && a[i] == a[l1]) Chmin(res, max({G(j, r1, l2, i - 1), G(l1 + 1, j - 1, i + 1, r2) + 1})); } return g[l1][r1][l2][r2] = max(2, res); } pii F(int l, int r) { if (l > r) return {0, 0}; if (f[l][r].second) return f[l][r]; pii res = min(F(l + 1, r) + pii{1, 1}, F(l, r - 1) + pii{1, 1}); for (int i = l; i < r; ++i) { if (i != l && a[i] == a[l]) for (int j = l + 1; j <= r; ++j) { int c = G(l + 1, min(i, j) - 1, max(i, j) + 1, r); pii d = i >= j ? F(j, i - 1) : F(i + 1, j); if (c < Inf) res = min(res, {d.first, max(d.second, max(2, c + 1))}); } if (i != r && a[i] == a[r]) for (int j = l; j < r; ++j) { int c = G(l, min(i, j) - 1, max(i, j) + 1, r - 1); pii d = i >= j ? F(j, i - 1) : F(i + 1, j); if (c < Inf) res = min(res, {d.first, max({2, d.second, c + 1})}); } } return f[l][r] = res; } signed main() { n = read(); for (int i = 1; i <= n; ++i) a[i] = read(); Print(stdout, F(1, n)); return 0; } ```