Day48【NOIP模拟赛13】

序列

很好的一道 “送命题” “签到题”,想了两个小时一分没有,破防了。

考场上想到了定义 d p i , j , 0 / 1 dp_{i,j,0/1} dpi,j,0/1 表示处理完前 i i i 个数,当前数为 j j j,是否被修改的方案数,然而不会去重,遂开摆。

所以为了 不重不漏 的统计方案数,我们增加一种状态 d p i , j , 2 dp_{i,j,2} dpi,j,2 表示当前位既有可能被修改,又有可能未被修改的方案数,请注意这种状态与其它两种状态并不存在所谓的包含关系,一定的情况就只能属于 0 , 1 0,1 0,1,可能的情况就只能属于 2 2 2

当然为了方便些,下文中 d p i , j , 0 / 1 / 2 dp_{i,j,0/1/2} dpi,j,0/1/2,分别表示表示处理完前 i i i 个数,当前位为 j j j,未被修改( 0 0 0),都有可能( 1 1 1),一定被修改( 2 2 2)的方案数。

现在考虑转移:

  • m m m 后面可以接任意数。
  • 1 1 1 可以接在任意数后面。
  • 如果上一位是一个被修改的不为 m m m 的数,当前位一定被修改。
  • 如果上一位未被修改,那么这一位要么不被修改,要么被修改为 1 1 1
  • 如果上一位被修改成了不是 m m m 的数 x x x,当前位要么为 1 1 1,要么为 x + 1 x + 1 x+1
  • 如果当前位为 0 0 0,上一位要么未被修改,要么被修改为了 m m m

把文字语言转化成状态转移方程式可得:

  • d p i , a i , 0 ← d p i , a i , 0 + d p i − 1 , j , 0 , j ∈ [ 0 , m ] dp_{i,a_i,0} \leftarrow dp_{i,a_i,0}+dp_{i-1,j,0},j\in[0,m] dpi,ai,0dpi,ai,0+dpi1,j,0,j[0,m]
  • d p i , 1 , 1 + ( a i ≠ 1 ) ← d p i , 1 , 1 + ( a i ≠ 1 ) + d p i − 1 , j , 0 , j ∈ [ 0 , m ] dp_{i,1,1+(a_i \ne 1)} \leftarrow dp_{i,1,1+(a_i \ne 1)}+dp_{i-1,j,0},j\in [0,m] dpi,1,1+(ai=1)dpi,1,1+(ai=1)+dpi1,j,0j[0,m]
  • d p i , 1 , 2 ← d p i , 1 , 2 + d p i − 1 , j , 2 , j ∈ [ 0 , m − 1 ] dp_{i,1,2} \leftarrow dp_{i,1,2}+dp_{i-1,j,2}, j\in[0,m-1] dpi,1,2dpi,1,2+dpi1,j,2,j[0,m1]
  • d p i , j , 1 + ( a i ≠ j ) ← d p i , j , 1 + ( a i ≠ j ) + d p i − 1 , m , 1 + d p i − 1 , m , 2 , j ∈ [ 2 , m ] dp_{i,j,1+(a_i \ne j)} \leftarrow dp_{i,j,1+(a_i \ne j)}+dp_{i-1,m,1}+dp_{i-1,m,2}, j\in[2,m] dpi,j,1+(ai=j)dpi,j,1+(ai=j)+dpi1,m,1+dpi1,m,2,j[2,m]
  • d p i , j + 1 , 2 ← d p i , j + 1 , 2 + d p i − 1 , j , 2 , j ∈ [ 0 , m − 1 ] dp_{i,j+1,2} \leftarrow dp_{i,j+1,2}+dp_{i-1,j,2}, j \in [0,m-1] dpi,j+1,2dpi,j+1,2+dpi1,j,2,j[0,m1]
  • d p i , j + 1 , 1 + ( a i ≠ j + 1 ) ← d p i , j + 1 , 1 + ( a i ≠ j + 1 ) + d p i − 1 , j , 1 , j ∈ [ 0 , m − 1 ] dp_{i,j+1,1+(a_i \ne j+1)} \leftarrow dp_{i,j+1,1+(a_i \ne j+1)}+dp_{i-1,j,1}, j \in [0,m-1] dpi,j+1,1+(ai=j+1)dpi,j+1,1+(ai=j+1)+dpi1,j,1,j[0,m1]
  • 由于 d p i − 1 , m , 0 dp_{i-1,m,0} dpi1,m,0 d p i − 1 , m , 2 dp_{i-1,m,2} dpi1,m,2 都没有后效性,即不会影响 [ i , n ] [i,n] [i,n] 这段数,所以转移前让 d p i − 1 , m , 0 ← d p i − 1 , m , 0 + d p i − 1 , m , 2 dp_{i-1,m,0} \leftarrow dp_{i-1,m,0} + dp_{i-1,m,2} dpi1,m,0dpi1,m,0+dpi1,m,2 没有什么实际意义,单纯方便一点。

Q1:

d p i , j , 1 + ( a i ≠ j ) = d p i − 1 , m , 1 + d p i − 1 , m , 2 , j ∈ [ 2 , m ] dp_{i,j,1+(a_i \ne j)}=dp_{i-1,m,1}+dp_{i-1,m,2},j \in [2,m] dpi,j,1+(ai=j)=dpi1,m,1+dpi1,m,2,j[2,m]

A1:

我们已经考虑完了当前位不修改的情况:dp_{i,a_i,0}。

所以当前位要么可能修改,要么一定修改,如果当前位置 a i = j a_i=j ai=j,那么就是可能的情况,否则就是一定修改的情况。

无论哪种都可以从 d p i − 1 , m , 1 + d p i − 1 , m , 2 dp_{i-1,m,1}+dp_{i-1,m,2} dpi1,m,1+dpi1,m,2 转移过来,因为 m m m 后面可以接任何数,然而无法从 d p i − 1 , m , 0 dp_{i-1,m,0} dpi1,m,0 转移过来,因为如果上一位为 m m m 且未被修改,那么当前位一定就不能被修改为 1 1 1 以外的数。

初始状态显然有: d p 1 , a 1 , 0 = d p 1 , 1 , ( 1 + a 1 ≠ 1 ) = 1 dp_{1,a_1,0}=dp_{1,1,(1+a_1 \ne 1)}=1 dp1,a1,0=dp1,1,(1+a1=1)=1
答案显然是 d p n , a n , 0 + d p n , m , 2 dp_{n,a_n,0}+dp_{n,m,2} dpn,an,0+dpn,m,2

具体实现上,使用滚动数组,其次,这道题有点卡常,所以尽量减少取模次数就行了。

这里只给出核心代码。

for (int j = 0; j <= m; j++) {
    add(dp[i & 1][a[i]][0], dp[i - 1 & 1][j][0]);
    add(dp[i & 1][1][w(i, 1)], dp[i - 1 & 1][j][0]);
    if (j >= 2) {
        add(dp[i & 1][j][w(i, j)], dp[i - 1 & 1][m][1]);
        add(dp[i & 1][j][w(i, j)], dp[i - 1 & 1][m][2]);
    }
    if (j < m) {
        add(dp[i & 1][j + 1][2], dp[i - 1 & 1][j][2]);
        add(dp[i & 1][j + 1][w(i, j + 1)], dp[i - 1 & 1][j][1]);
        add(dp[i & 1][1][2], dp[i - 1 & 1][j][2]);
    }
 }

贪吃蛇

真正意义上的签到题,然而少打了个记忆化挂了 20 p t s 20pts 20pts

考虑单独处理每一个 ( i , j ) (i,j) (i,j),用类似于 b f s bfs bfs 方式扩展当前能走到的区域,用优先队列存储未被暂时不能吃掉的点,然后乱搞,当然有两个剪枝:

  • 当前权值大于矩阵最大权值,显然合法。
  • 走到了一个已经处理过的合法格子,显然当前 ( i , j ) (i,j) (i,j) 也跟着合法了。

最坏时间复杂度: O ( n 2 m 2 log ⁡ n m ) O(n^2m^2\log nm) O(n2m2lognm),然而跑的飞快,我最慢的点只跑了 200 m s 200ms 200ms,比 std 快了十倍。 (雾

蛋糕

神仙题,当然仅指 zdj 学长的做法,std 做法啥也不是。

首先看到这道题,很自然得到一个朴素 O ( n V ) O(nV) O(nV) d p dp dp
d p i , j dp_{i,j} dpi,j 表示将 [ 1 , i ] [1,i] [1,i] 全部变为 0 0 0,且使用了 j j j 次横切操作的最小代价。
然后有转移 d p i , j = min ⁡ ( d p i − 1 , j + max ⁡ ( a i − j , 0 ) , d p i , j − 1 + s u f i + 1 ) dp_{i,j}=\min(dp_{i-1,j}+\max(a_i-j,0),dp_{i,j-1}+suf_{i+1}) dpi,j=min(dpi1,j+max(aij,0),dpi,j1+sufi+1),其中 s u f suf suf 表示后缀最大值。

先说结论,每一个 d p i dp_i dpi 都是一个凸壳,凸壳的横坐标为 j j j,其实这可以通过感性理解猜测得到,即横切次数多了少了都不优,要恰好处于某个中间位置才最优。不过后续会给出严格证明。

观察状态转移方程式,我们分开考虑两种转移:

1. d p i − 1 , j + max ⁡ ( a i − j , 0 ) dp_{i-1,j}+\max(a_i-j,0) dpi1,j+max(aij,0)

这实质上是通过 d p i − 1 dp_{i-1} dpi1 这一凸壳得到了新一个凸壳 d p i ′ dp_i' dpi

在这里插入图片描述

假设 d p i − 1 dp_{i-1} dpi1 长这个样子,由于有一个 max ⁡ \max max,把其拆开得到:

  • d p i − 1 , j ← d p i − 1 , j − j + a i , j ≤ a i dp_{i-1,j} \leftarrow dp_{i-1,j}-j+a_i, j \le a_i dpi1,jdpi1,jj+ai,jai
  • d p i − 1 , j dp_{i-1,j} dpi1,j 不变, j > a i j>a_i j>ai

这就意味着 a i a_i ai 右边那部分是不变的,所以我们只考虑红线左边这一部分。
左边这一部分每一个 d p dp dp 值都加上了 a i − j a_i-j aij,显然 Δ y \Delta y Δy 减了 1 1 1,那么就是所有线段的斜率减了 1 1 1。直观表示出来左边部分就变成绿色那段了,

总结一下,这种转移就相当于把 d p i − 1 dp_{i-1} dpi1 这一凸壳中 x < a i x < a_i x<ai 部分的所有线段的斜率减了 1 1 1,当然还可能把某条线段一分为二了。

2. d p i , j − 1 + s u f i + 1 dp_{i,j-1}+suf_{i+1} dpi,j1+sufi+1

实质上是 d p i dp_{i} dpi 自己更新自己,大致如下图。
在这里插入图片描述

相当于从每个点延伸出一条 k = s u f i + 1 k=suf_{i+1} k=sufi+1 的斜线,然后与后面的每一个点比一个 min ⁡ \min min,我们注意到,上面那条红线是没有任何贡献的,而下面那条切线可以,为什么呢?
因为斜率是单增的,我们实质上是把一段极长的满足 k k k 大于 s u f i + 1 suf_{i+1} sufi+1 的后缀推平为了 s u f i + 1 suf_{i+1} sufi+1,而这段后缀就是切线切到的部分。

总结一下,这种转移就相当于把某段后缀推平为 s u f i + 1 suf_{i+1} sufi+1,还是很好理解的。

现在,我们知道了, d p dp dp 转移的实质是某段前缀斜率减 1 1 1 ,某条线段一分为二,某段后缀斜率推平,这也恰好证明了为什么 d p i dp_i dpi 是凸壳,因为这两种操作不改变斜率单调递增这一性质。其次,这还带给了我们更好入手的另一性质:相邻两个线段的斜率差只可能为 1 1 1

这启发我们什么???我们根本不需要记录每一条线段的具体斜率,我们只需要用优先队列维护这些断点(即 a i a_i ai)和当前最大斜率即可!!!我们每次都将 a i a_i ai 加入队列,然后,我们每次都将优先队列中的断点弹出来,直到最大斜率小于等于 s u f i + 1 suf_{i+1} sufi+1 或队列为空,因为每少一个断点最大斜率减 1 1 1
如果队列为空,那么最大斜率就为 s u f i + 1 suf_{i+1} sufi+1,否则一定存在一段斜率为 s u f i + 1 suf_{i+1} sufi+1 的线段,因为斜率在值域上是连续的。所以,这个性质是不受操作影响的,非常美妙。

初始情况凸壳中只有一条直线 k = s u f 1 k=suf_1 k=suf1,队列中没有断点,最大斜率为 s u f 1 suf_1 suf1

Last but not least

现在来考虑答案怎么求。

我们注意到,当进行完 n n n d p dp dp 转移后,因为 s u f n + 1 = 0 suf_{n+1}=0 sufn+1=0,所以最后凸壳中线段的斜率一定小于等于 0 0 0。因此我们最终得到了一个半凸壳,且一定是下面着这种。

在这里插入图片描述
此时的最高点为 d p n , 0 dp_{n,0} dpn,0,也就是 ∑ a i \sum a_i ai,然后这些线段的斜率从左到右分别为 − k , − ( k − 1 ) , − ( k − 2 ) , . . . , 0 -k,-(k-1),-(k-2),...,0 k,(k1),(k2),...,0,由此,我们可以推出最低点的 y y y 坐标为 ∑ i = 1 n a i − ∑ i = 0 k − ( k − i ) × ( x i − x i − 1 ) \sum_{i=1}^{n} a_i-\sum_{i=0}^{k}-(k-i)\times(x_i-x_{i-1}) i=1naii=0k(ki)×(xixi1),其中 x i x_i xi 为凸包中断点的横坐标,下标从零开始且 x 0 = 0 x_0=0 x0=0

化简后得到 A n s = ∑ i = 1 n a i − ∑ i = 0 k x i Ans=\sum_{i=1}^n a_i-\sum_{i=0}^{k}x_i Ans=i=1naii=0kxi

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 1e6 + 5;
int n, a[MAXN], suf[MAXN], derta;
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = n; i >= 1; i--) suf[i] = max(suf[i + 1], a[i]);
    int Maxy = suf[1]; // 最大斜率
    priority_queue<int> q;
    for (int i = 1; i <= n; i++) {
        derta += a[i], q.push(a[i]);
        while (!q.empty() && Maxy > suf[i + 1]) q.pop(), Maxy--; // 删除断点,等价于推平凸壳的某段后缀
        if (q.empty())
            Maxy = suf[i + 1];
    }
    while (!q.empty()) derta -= q.top(), q.pop();
    cout << derta;
    return 0;
}

代码很简洁但很难懂,又或者是我太菜了。

至此,这道题用薄纱 std 的做法完美通过了,好题好题。

简单树上计数

大致思路应该是设计一种树哈希,使得其可以支持删点与加点之类的操作,然后判断树同构即可。

代码量好像有一点点小,std 只写了 400 400 400 ,那就不补了。

posted @ 2024-10-09 22:21  Fracture_Dream  阅读(17)  评论(0)    收藏  举报  来源