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,0←dpi,ai,0+dpi−1,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)+dpi−1,j,0,j∈[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,2←dpi,1,2+dpi−1,j,2,j∈[0,m−1]。
- 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)+dpi−1,m,1+dpi−1,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,2←dpi,j+1,2+dpi−1,j,2,j∈[0,m−1]。
- 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)+dpi−1,j,1,j∈[0,m−1]。
- 由于 d p i − 1 , m , 0 dp_{i-1,m,0} dpi−1,m,0 和 d p i − 1 , m , 2 dp_{i-1,m,2} dpi−1,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} dpi−1,m,0←dpi−1,m,0+dpi−1,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)=dpi−1,m,1+dpi−1,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} dpi−1,m,1+dpi−1,m,2 转移过来,因为 m m m 后面可以接任何数,然而无法从 d p i − 1 , m , 0 dp_{i-1,m,0} dpi−1,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(dpi−1,j+max(ai−j,0),dpi,j−1+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) dpi−1,j+max(ai−j,0)
这实质上是通过 d p i − 1 dp_{i-1} dpi−1 这一凸壳得到了新一个凸壳 d p i ′ dp_i' dpi′

假设 d p i − 1 dp_{i-1} dpi−1 长这个样子,由于有一个 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 dpi−1,j←dpi−1,j−j+ai,j≤ai
- d p i − 1 , j dp_{i-1,j} dpi−1,j 不变, j > a i j>a_i j>ai
这就意味着
a
i
a_i
ai 右边那部分是不变的,所以我们只考虑红线左边这一部分。
左边这一部分每一个
d
p
dp
dp 值都加上了
a
i
−
j
a_i-j
ai−j,显然
Δ
y
\Delta y
Δy 减了
1
1
1,那么就是所有线段的斜率减了
1
1
1。直观表示出来左边部分就变成绿色那段了,
总结一下,这种转移就相当于把 d p i − 1 dp_{i-1} dpi−1 这一凸壳中 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,j−1+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,−(k−1),−(k−2),...,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=1nai−∑i=0k−(k−i)×(xi−xi−1),其中
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=1nai−∑i=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 行 ,那就不补了。

浙公网安备 33010602011771号