CF1649F – Serious Business – 题解与思路重现

CodeForces Round #755 Div.2 F / Div.1 D Serious Business

此题整整困了我一天半,耗费了好几张草稿纸。实在走投无路,翻看题解,却发现正解曾经近在咫尺。有必要稍作整理。

考虑DP。

错误转移1

这道题与清理班次(《算法竞赛进阶指南》上的一道例题)过于相似,都是“给定一些段落[lp,rp][l_p, r_p],选择其中一些完整覆盖[l0,r0][l_0,r_0],求最小花费”的形式。记上述花费为f(l0,r0)f(l_0, r_0)。但本题与原题不一样之处在于其起点和终点是任意给定的。我的第一反应是枚举在x2x_2位置从第22行转到第33行,利用数据结构维护所有x1x2x_1\leq x_2,在x1x_1转移到第二行,解锁到达x2x_2的最小代价。对于各行前缀和,我们可以利用线段树或树状数组维护。同时我们也知道可以利用数据结构优化在O(nlogn)\mathrm{O}(n \log n)的时间内,计算出对于一个固定的起点l0l_0,所有f(l0,r0),r0[l0,n],p,rp=r0f(l_0, r_0), r_0 \in [l_0, n], \exists p, r_p=r_0的值。

但经过一上午的死缠烂打,我发现根本无法维护邻项的ff值。或者换句话讲,其二者甚至没有必然关联。考虑它的转移:如果存在一个段[lp,rp][l_p, r_p]花费为cpc_p,则可以更新f(l0,rp)mini[l1,rp)f(l0,i)+cpf(l_0, r_p)\leftarrow \min\limits_{i \in [l-1, r_p)} {f(l_0, i) + c_p}。也就是说,我们在长度不尽相等的若干区间内取极值后作转移。然而随着l0l_0发生改变,一些区间变得不再可用(右端点在l0l_0左侧),而一些区间在最开始就可以被利用(其包含l0l_0)。最致命的是,由于此处的最值不具有传递性,重新计算所有转移路径上的取值,还不如重新暴力求一遍f(l0,r0)f(l_0′,r_0)呢。时间复杂度O(n2logn)\mathrm{O}(n^2 \log n)

从上午八点到十二点半,反复确认打表后,这个朴素的DP告吹了。

生成了一些随机的区间,从上到下分别暴力计算了f(1,n)f(1,n)f(n,n)f(n,n)。可以发现,相邻两行之间的值难以用数据结构维护。

错误转移2

一开始做原题时,就思考过一个问题:如果我们设计的状态不包含任何关于最后选择的段落的信息,就会造成“无代价转移”或者“重复计算代价转移”。

无代价转移:考虑两个区间[l1,r1],[l2,r2],l1l2r1r2[l_1,r_1],[l_2,r_2],l_1\leq l_2 \leq r_1 \leq r_2。有i[l1,l2],i[l2,r1],i[l2,r2]i\in [l_1,l_2], i’ \in [l_2,r_1], {i’}’ \in [l_2, r_2]。则如果我们秉承“相同区间内解锁不重复计费”的概念,会出现f(i)0f(i)0f(i)f({i’}’)\stackrel{0}{\longleftarrow} f(i’)\stackrel{0}{\longleftarrow} f(i)的转移。

重复计算代价转移:如果我们总是在某一段不再可用(i=rp+1i=r_p+1)时,计入当前处在的某一段的费用,那么当f(i)f(i’)实际上选择了c2c_2时,转移到f(i)f({i’}’)时还会再计算一遍。

而且,我们仅仅考虑的是,不管以什么方式到达a2,ia_{2,i},获得的收益都最大。因此,我们没有必要关注具体是从哪个位置跳到第22行,然后转移到a2,ia_{2,i}的。

所以,不论怎么样,我们都需要——至少隐式地——记录当前最后付费区间的相关信息。一个更加朴素的DP状态已经呼之欲出了:f(i,j)f(i,j)表示从a1,1a_{1,1}走到a2,ia_{2,i},同时最后付费的区间编号为jj的最大收益。存在以下转移:f(i,j)=a2,i+max{{f(i1,j),lj<irjf(i1,p)+cj,rp=i1,k=1ia1,k+cj}f(i,j)= a_{2,i}+ \max\left\{\begin{cases}f(i-1,j), & l_j<i\leq r_j \newline f(i-1,p)+c_j, & r_p=i-1\end{cases},\quad \sum\limits_{k=1}^{i}a_{1,k}+c_j \right\}

我们发现它的转移非常奇怪,全部都只依赖f(i1,?)f(i-1, ?)。这启发我们,看它是否能通过数据结构高效维护邻项DP值。但是……对于每一个jj,都有不一样的cjc_j参与转移,除非使用非常复杂的延迟标记系统并且在每一段的结尾处处理第一类转移(至少我是写不出来的),几乎断掉了区间修改、查询的可能性。所以,时间复杂度上限的下限是O(nq)\mathrm{O}(nq)(实际上可能卡不到?!)。

整一个下午都在考虑的转移又泡汤了。

不完全正确的转移

由于我们不关心从哪里转移到第二行,同时还要确保每一段的费用被合理计算,那干脆……先放弃掉在每一段内部的转移,也即只考虑到达每一段结尾时分数最大值,继续改造原题的DP。

f(i)f(i)表示到达a2,ia_{2,i}时积累的最多分数。其中,p,rp=i\exists p, r_p=i。由于这是每一段的末尾,转移就具有了唯一性。存在以下转移:f(i)=max{maxj[lp1,rp){f(j)+k=jia2,k},maxj[lp,rp]{k=0ja1,k+l=jia2,l}}cpf(i)=\max\left\{\max\limits_{j\in [l_p-1, r_p)}\left\{f(j)+\sum\limits_{k=j}^{i}a_{2,k}\right\}, \max\limits_{j \in [l_p, r_p]} \left\{\sum\limits_{k=0}^{j}a_{1,k}+\sum\limits_{l=j}^{i}a_{2,l}\right\}\right\}-c_p

至少我们写出来了。通俗的说,我们可以从该段内部利用前序状态,加上从某一段结尾到该段结尾的所有分数;或者在该段中的某一位置从第11行跳下来。两种方式都不重不漏地计算了到该段末尾需要计入该段费用的所有方案。时间复杂度O(nlogn)\mathrm{O}(n \log n)

但想必上式中间的一坨前缀和的维护已经够麻烦了,还要处理一个棘手的问题:现在只能从每一段的末尾转移到第三段去,那段落中央的又如何计算?一筹莫展。

完全不正确的转移

昨天深夜脑子抽风,又跑去把在上文中枪毙的状态(f(i)f(i)表示不知晓最后付费的具体区间时到达a2,ia_{2,i}的最大收益)推导了好几遍。

正解

考虑刚刚的转移。假如我们代码能力足够强,真的稳健地维护了前缀和及其极值,然后成功计算了那一堆复杂的取极值的式子,它唯一的毛病在于:不包含从某一段中间转移到第33的方案。有没有办法单独计算呢?

在此之前,先要对我们上文的式子做一些简化。记pre(1/2/3,i)=j=1ia1/2/3,j\text{pre}(1/2/3,i)=\sum\limits_{j=1}^{i}a_{1/2/3,j},则对于某一种方案,在x1x_1处转移到第22行,x2x_2处转移到第33行,其收益为:pre(1,x1)+pre(2,x2)pre(2,x11)+pre(3,n)pre(3,x21)+cost(x1,x2)\text{pre}(1,x_1)+\text{pre}(2,x_2)-\text{pre}(2,x_1-1)+\text{pre}(3,n)-\text{pre}(3,x_2-1)+\text{cost}(x_1,x_2),其中cost(x1,x2)\text{cost}(x_1,x_2)表示将[x1,x2][x_1,x_2]全部解锁的最小代价——这正是我们在错误转移1中尝试直接维护但宣告失败的东西。但可以肯定,只要x1x_1固定,最终收益必然包含prec(1,x1)=pre(1,x1)pre(2,x11)\text{prec}(1,x_1)=\text{pre}(1,x_1)-\text{pre}(2,x_1-1);但凡我们决定在x2x_2处切入第33行,最终收益必然包含prec(2,x2)=pre(2,x2)+pre(3,n)pre(3,x21)\text{prec}(2,x_2)=\text{pre}(2,x_2)+\text{pre}(3,n)-\text{pre}(3,x_2-1)

现在,假若我们要重写一遍上述式子,那就方便多了:

f(i)=max{maxj[lp1,rp)f(j),maxj[lp,rp]prec(1,j)}cp,p,rp=if(i)=\max\left\{\max\limits_{j\in [l_p-1,r_p)}f(j), \max\limits_{j \in [l_p, r_p]}\text{prec}(1,j) \right\}-c_p, \quad \exists p, r_p=i

考虑某个具体的方案,它一定至少解锁了一个段落才得以跳转到第33行。那么考虑它倒数第二个解锁的段落为pp,最后解锁的段落为ggrplgr_p \geq l_g,显然该方案应该走完了rpr_p——否则不必解锁gg段落。那么,它的最终代价就是f(rp)+prec(2,x2)cg,rp<x2rgf(r_p)+\text{prec}(2,x_2)-c_g, r_p<x_2\leq r_g。固定最终段落gg,那么问题就转变成了求maxlg1ijrg{f(i)+prec(2,j)cg}\max\limits_{l_g-1\leq i \leq j \leq r_g} \left\{f(i)+\text{prec}(2,j)-c_g\right\}。再来考虑只解锁一个段落的情况,即可转化为maxlgijrg{prec(1,i)+prec(2,j)cg}\max\limits_{l_g\leq i\leq j \leq r_g} \{\text{prec}(1,i)+\text{prec}(2,j)-c_g\}

我们惊奇地发现,prec\text{prec}函数是常量;f(j),jrgf(j), j\leq r_gf(rg)f(r_g)计算之前就已经更新完成,无后效性。它可以在线段树上分治求解!更具体地,我们保存每个区间内部的这些式子的答案。对于两个相邻区间的并,直接由定义计算符合相应位置关系的最值,并更新最终答案即可。

至此,本题终于告一段落。它检验了对前缀和优化区间求和、DP转移的无后效性和状态的最优性、线段树上分治这些重要技巧的灵活运用——而我并没有悉数掌握。这就是综合题“每个算法和技巧都很普通,组合到一起却没法把控”的精妙吧。

Submission #148765401

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
#include <bits/stdc++.h> using namespace std; template <typename T> inline bool read (T &x) { x = 0; int f = 1; char c = getchar (); for (; c != EOF && !isdigit (c); c = getchar ()) if (c == '-') f = -1; if (c == EOF) return 0; for (; c != EOF && isdigit (c); c = getchar ()) x = (x<<1) + (x<<3) + (c^48); x *= f; return 1; } template <typename T, typename... Targs> inline bool read (T &x, Targs&... args) { return read (x) && read (args...); } template <typename T> void print (T x) { if (x < 0) putchar ('-'), x = -x; if (x > 9) print (x / 10); putchar ('0' + x % 10); } template <typename T, typename... Targs> inline void print (T x, Targs... args) { print (x), putchar (' '), print (args...); } #define reint register int #define inl inline #define newl putchar('\n') typedef long long ll; typedef unsigned long long ull; // typedef __int128 lll; // typedef long double llf; typedef pair <int, int> pint; constexpr int N = 5e5 + 10; constexpr ll INF = LLONG_MAX>>2; int n, m, a[3][N], l, r, c; ll pre[3][N], pc[2][N], ans = -INF, f; struct offer { int l, r, c; bool operator < (const offer &b) const { return r < b.r; } } g[N]; template <typename T> inl void gomx (T &x, T y) { x = max (x, y); } struct seg_tree { struct info { ll f, p[2], psum, fsum; } const INULL { -INF, {-INF, -INF}, -INF, -INF }; struct node { info dta; int l, r; } t[N<<2]; inl info select (info &a, info &b) { return { max (a.f, b.f), { max (a.p[0], b.p[0]), max (a.p[1], b.p[1]) }, max (max (a.psum, b.psum), a.p[0] + b.p[1]), max (max (a.fsum, b.fsum), a.f + b.p[1]) }; } #define post(x) (t[x].dta = select (t[x<<1].dta, t[x<<1|1].dta)) void build (int x, int l, int r) { t[x] = { INULL, l, r }; if (l == r) { t[x].dta = { -INF, { pc[0][l], pc[1][l] }, pc[0][l] + pc[1][l], -INF }; return; } int mid = l + r >> 1; build (x<<1, l, mid); build (x<<1|1, mid + 1, r); post (x); } void upd_f (int x, int tar, ll _f) { if (t[x].l == t[x].r) { gomx (t[x].dta.f, _f); gomx (t[x].dta.fsum, t[x].dta.p[1] + _f); return; } int mid = t[x].l + t[x].r >> 1; upd_f (tar <= mid ? x<<1 : x<<1|1, tar, _f); post (x); } info query (int x, int L, int R) { if (t[x].l >= L && t[x].r <= R) return t[x].dta; int mid = t[x].l + t[x].r >> 1; info lres = INULL, rres = INULL; if (L <= mid) lres = query (x<<1, L, R); if (R > mid) rres = query (x<<1|1, L, R); return select (lres, rres); } #undef post } seg; seg_tree::info res; int main () { /* CF1649F - Serious Business 吴秋实 */ read (n, m); for (int i = 0; i < 3; ++i) for (int j = 1; j <= n; ++j) read (a[i][j]), pre[i][j] = pre[i][j - 1] + a[i][j]; pc[0][0] = pc[0][1] = -INF; for (int i = 1; i <= n; ++i) pc[0][i] = pre[0][i] - pre[1][i - 1], pc[1][i] = pre[2][n] + pre[1][i] - pre[2][i - 1]; seg.build (1, 0, n); for (int i = 1; i <= m; ++i) read (g[i].l, g[i].r, g[i].c); sort (g + 1, g + m + 1); // 右端点递增排序。 for (int i = 1; i <= m; ++i) { res = seg.query (1, l = g[i].l, r = g[i].r); gomx (ans, res.psum - (c = g[i].c)); // 只采用一段的计费。 f = res.p[0] - c; // (直接从段内转移下来的)更新dp值。 res = seg.query (1, l - 1, r); gomx (ans, res.fsum - c); // 前序转移而来的计费。 res = seg.query (1, l - 1, r - 1); gomx (f, res.f - c); // 更新dp值。 seg.upd_f (1, r, f); } print (ans); return 0; }
  • 2022年3月8日