0%

题目大意

题目描述

给定一个长度为 nn 的数组 AA 以及一个阈值 mm。你需要维护这个数组并支持以下两种操作,共执行 QQ 次:

  • 1 L R X:将区间 [L,R][L, R] 内的所有元素减去 XX(即对于 LiRL \le i \le R,执行 AiAiXA_i \leftarrow A_i - X)。

  • 2 L R:将区间 [L,R][L, R] 内的所有元素右移一位(相当于除以 22 向下取整,即对于 LiRL \le i \le R,执行 AiAi2A_i \leftarrow \lfloor \frac{A_i}{2} \rfloor)。

在每一次操作结束后,如果整个数组中存在至少一个元素小于等于阈值 mm,则会触发一次“时空回溯”:

  1. 全局重置次数 kk 的值增加 11

  2. 整个数组 AA 立即恢复为最开始的初始状态。

要求在处理完所有给定的 QQ 次操作后,输出最终的重置次数 kk(初始时 k=0k=0)。

数据范围

  • 数据组数 T10T \le 10

  • 1n,Q1051 \le n, Q \le 10^5

  • 0m<2300 \le m < 2^{30}

  • 初始数组满足 m<Ai<230m < A_i < 2^{30}

  • 对于操作 1:1LRn1 \le L \le R \le n0X<2300 \le X < 2^{30}

  • 对于操作 2:1LRn1 \le L \le R \le n

样例输入

1
2
3
4
5
6
1
3 1 3
5 4 6
1 1 2 4
2 2 3
1 1 3 5

样例输出

1
2

样例解释

初始时数组为 [5,4,6][5, 4, 6],阈值 m=1m = 1,重置次数 k=0k = 0

  • 11 次操作(1 1 2 4):对区间 [1,2][1, 2] 的元素减去 44,数组变为 [1,0,6][1, 0, 6]。此时存在元素(1100)小于等于阈值 11,触发回溯。重置次数 k1k \leftarrow 1,数组重置为初始状态 [5,4,6][5, 4, 6]

  • 22 次操作(2 2 3):对区间 [2,3][2, 3] 的元素右移一位,数组变为 [5,2,3][5, 2, 3]。此时数组中所有元素均大于 11,不触发回溯。

  • 33 次操作(1 1 3 5):对区间 [1,3][1, 3] 的元素减去 55,数组变为 [0,3,2][0, -3, -2]。此时存在元素小于等于阈值 11,触发回溯。重置次数 k2k \leftarrow 2,数组重置为初始状态 [5,4,6][5, 4, 6]

所有操作执行完毕,最终的重置次数 kk22

思路讲解

gemini 的这个数学功底比较好,直接合并了这个位移操作和这个减法操作。

PDF

image

这个向下嵌套性质不是那么容易可以看出来的,那么我让 gemini 写了一个完整的,直观的,而且也是非常严谨的这个解释,还是非常好的:

image

不过我们是用题解的这个做法

image

我们可以维护一个这个标记数组,

AC代码

AC
https://acm.hdu.edu.cn/contest/view-code?cid=1199&rid=17893

心路历程(WA,TLE,MLE……)

PDF

一般来说,一个线段树只应该有一个 merge 函数,因为我们的 merge 是 static 的,我们的逻辑也是用 merge 从子节点中完全重新推出父节点

vector clear() 函数减少内存的重新分配(保留 capacity),进而加快速度

类似于 t.op_ls[i].type,这样子比较复杂的东西,可以使用引用,重命名为 t_op_ls[i].type,这样子书写的时候可以少一层,可以减少写错的可能性

注意特殊操作的范围,不一定就局限在【l,r】了,可能是全局操作【1,N】。

常见错误之除零错误。

https://acm.hdu.edu.cn/contest/view-code?cid=1199&rid=17130

image

一般来说,只要除数不是一个简单的,一个变量,都要担心这个除 0 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void gen_eraly_seg() {
for (int i = 1; i <= N; ++i) {
ll early_sz;
if (i == 1) {
early_sz = K - (bound_win + K - 1) / K;
} else {
if (early_sz_ls[i - 1] - K == 0) {
early_sz = K;
} else {
// 这里这个除法要小心除 0 错误。
early_sz = (bound_win - K * K) / (early_sz_ls[i - 1] - K);
}
}
early_sz = min(early_sz, K);
early_sz_ls[i] = early_sz;
for (int j = 1; j <= early_sz; ++j) {
ans_mat[i][j] = idx;
}
if (early_sz != 0) {
++idx;
}
}
}

Lucas 定理是怎么来的?

Lucas 定理的最直观证明是通过生成函数(母函数)和二项式定理来推导的。

首先,根据二项式定理,我们知道组合数 CnmC_n^m 其实就是多项式 (1+x)n(1+x)^n 展开后 xmx^m 这一项的系数。

对于任意质数 pp,考虑 (1+x)p(modp)(1+x)^p \pmod p 的展开式:

(1+x)p=Cp0x0+Cp1x1+Cp2x2++Cppxp(1+x)^p = C_p^0 x^0 + C_p^1 x^1 + C_p^2 x^2 + \dots + C_p^p x^p

因为 pp 是质数,当 1ip11 \le i \le p-1 时,Cpi=p!i!(pi)!C_p^i = \frac{p!}{i!(p-i)!} 的分子里有一个 pp,而分母里没有 pp 可以把它约掉。所以中间所有的项都是 pp 的倍数,在模 pp 意义下全部等于 00
因此我们得到了一个非常漂亮的核心结论:

(1+x)p1+xp(modp)(1+x)^p \equiv 1 + x^p \pmod p

现在我们来求 (1+x)n(modp)(1+x)^n \pmod pxmx^m 的系数。
我们将 nnmm 拆分成除以 pp 的商和余数(类似于把它们转成 pp 进制):
n=n/p×p+(nmodp)n = \lfloor n/p \rfloor \times p + (n \bmod p)
m=m/p×p+(mmodp)m = \lfloor m/p \rfloor \times p + (m \bmod p)

代入多项式中:

由于前面证明的核心结论,把内部的 (1+x)p(1+x)^p 替换掉:

(1+x)n=(1+x)n/p×p+(nmodp)=((1+x)p)n/p×(1+x)nmodp \begin{aligned}(1+x)^n &= (1+x)^{\lfloor n/p \rfloor \times p + (n \bmod p)} \\&= \left( (1+x)^p \right)^{\lfloor n/p \rfloor} \times (1+x)^{n \bmod p} \\\end{aligned}

(1+x)n(1+xp)n/p×(1+x)nmodp(modp) \begin{aligned}(1+x)^n &\equiv (1+x^p)^{\lfloor n/p \rfloor} \times (1+x)^{n \bmod p} \pmod p\end{aligned}

我们要求的是 xmx^m 的系数,而 xm=(xp)m/p×xmmodpx^m = (x^p)^{\lfloor m/p \rfloor} \times x^{m \bmod p}
所以在右边这两个括号相乘时,我们必须:

  1. 从前一个括号 (1+xp)n/p(1+x^p)^{\lfloor n/p \rfloor} 中选出 (xp)m/p(x^p)^{\lfloor m/p \rfloor},这部分的系数是 Cn/pm/pC_{\lfloor n/p \rfloor}^{\lfloor m/p \rfloor}

  2. 从后一个括号 (1+x)nmodp(1+x)^{n \bmod p} 中选出 xmmodpx^{m \bmod p},这部分的系数是 CnmodpmmodpC_{n \bmod p}^{m \bmod p}

两者相乘,就得到了 xmx^m 的总系数。这就是 Lucas 定理的来源:

CnmCn/pm/p×Cnmodpmmodp(modp)C_{n}^{m} \equiv C_{\lfloor n / p \rfloor}^{\lfloor m / p \rfloor} \times C_{n \bmod p}^{m \bmod p} \pmod p


关于这道 AcWing 题目与你的模板

注意观察这道题目的输入格式:每一组询问都会给出一个不同的质数 pp

我们刚才在你的 main.tex 里,把 Lucas 定理改成了复用你已有的 Comb 命名空间和全局的 mod。那个模板只适用于所有询问的模数 pp 固定不变的情况(因为我们需要针对固定的 mod 预处理阶乘)。

对于这道题,因为 pp 会变(最高到 10510^5),所以我们不能用全局预处理,反而应该用我们刚才撤掉的那份按定义暴力算的代码!因为暴力算单次是 O(p)O(p) 的,加上 Lucas 递归,总时间大概是 O(plogpn)O(p \log_p n),这道题 n20n \le 20 组询问,最大运算量在 2×1062 \times 10^6 左右,完全可以光速通过。

以下是可以直接通过这道题的 AC 代码,风格和我们刚才讨论的完全一致:

AC

https://www.acwing.com/problem/content/submission/code_detail/44735227/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <iostream>
#include <vector>

using namespace std;
using ll = long long;

ll mod;

// 你的组合数模板
namespace Comb{
vector<ll> frac, infrac;
int ln = 1;
bool initialized = false;

inline ll binpow(ll a, ll k) {
ll res = 1;
a %= mod;
while (k > 0) {
if (k & 1) res = (res * a) % mod;
a = (a * a) % mod;
k >>= 1;
}
return res;
}

void Comb(int n) {
ln = n;
frac.assign(n + 5, 1);
infrac.assign(n + 5, 1);
for (int i = 1; i <= ln; ++i) {
frac[i] = (frac[i - 1] * i) % mod;
}
infrac[ln] = binpow(frac[ln], mod - 2);
for (int i = ln - 1; i >= 0; --i) {
infrac[i] = (infrac[i + 1] * (i + 1)) % mod;
}
}

inline ll CC(ll n, ll k) {
if (k < 0 || k > n) return 0;
if(!initialized){
// 【核心细节】:Lucas定理的余数最大为 mod-1,所以只用预处理到 mod-1。
// 绝对不能预处理到 mod,否则 frac[mod] = 0,会导致所有的 infrac 变成 0。
Comb(mod - 1);
initialized = true;
}
ll res = frac[n];
res = (res * infrac[n - k]) % mod;
res = (res * infrac[k]) % mod;
return res;
}
}
using namespace Comb;

// Lucas 定理递归求解,复用 CC 模板
inline ll lucas(ll a, ll b) {
if (a < mod && b < mod) return CC(a, b);
return CC(a % mod, b % mod) * lucas(a / mod, b / mod) % mod;
}

void solve() {
ll a, b;
cin >> a >> b >> mod;

// 每次 mod 发生变化,必须重置初始化标记
initialized = false;

cout << lucas(a, b) << '\n';
}

int main() {
// 优化输入输出
ios::sync_with_stdio(false);
cin.tie(nullptr);

int n;
if (cin >> n) {
while (n--) {
solve();
}
}
return 0;
}

题目大意

题目描述
需要构造 nn 个具有 kk 个面的骰子,每个面上的数字取值范围为 0m0 \sim m
两个骰子进行对决时,随机掷出一个面,点数大者获胜。要求构造的骰子组合必须同时满足以下三个条件:

  1. 循环劣势:形成 123n1n11 \to 2 \to 3 \to \dots \to n-1 \to n \to 1 的胜负关系。即 1 号骰子输给 2 号,2 号输给 3 号,……,nn 号输给 1 号(“输给”定义为在 k×kk \times k 种可能的掷出组合中,获胜的次数严格少于对方)。

  2. 绝对无平局每个数字至多只能出现在一个骰子上,保证任意两个骰子对决时不可能掷出相同的点数。

  3. 字典序最小:将 1 号到 nn 号骰子上的所有数字按顺序拼接成一个长度为 n×kn \times k 的序列,要求该序列在所有合法方案中字典序最小。

输入格式
第一行输入一个正整数 TT,表示测试数据组数。
接下来每组数据输入三个正整数 nnmmkk,分别表示骰子数量、面上的最大允许数字、每个骰子的面数。

输出格式
对于每组数据,输出一行 n×kn \times k 个用空格分隔的整数,依次表示 1 号到 nn 号骰子各个面上的数字。

样例数据

输入样例:

1
2
1
3 4 3

输出样例:

1
0 3 3 1 1 4 2 2 2

样例解释
输入代表需要构造 33 个骰子,每个骰子有 33 个面,数字范围在 040 \sim 4 之间。根据输出样例,三个骰子的构造如下:

  • 1 号骰子:[0, 3, 3]

  • 2 号骰子:[1, 1, 4]

  • 3 号骰子:[2, 2, 2]

胜负关系验证(总对决情况数为 3×3=93 \times 3 = 9 种):

  • 1 号 vs 2 号:1 号获胜的情况为 3>1(共 4 次),2 号获胜的情况为 1>01>04>04>34>3(共 5 次)。2 号胜率更高,1 号输给 2 号

  • 2 号 vs 3 号:2 号获胜的情况为 4>2(共 3 次),3 号获胜的情况为 2>1(共 6 次)。3 号胜率更高,2 号输给 3 号

  • 3 号 vs 1 号:3 号获胜的情况为 2>0(共 3 次),1 号获胜的情况为 3>2(共 6 次)。1 号胜率更高,3 号输给 1 号

该方案数字各不相同(无平局),构成了 12311 \to 2 \to 3 \to 1 的循环劣势,且可以证明拼接后的序列 0 3 3 1 1 4 2 2 2 是所有满足条件的方案中字典序最小的。

思路讲解

image

首先,我们为什么会想到双段构造?呃,其实正常的思维链路应该是,我们先想怎么构造,

其实题面里面讲的非常清楚,一个骰子最多只会有两种不同的数字,每个数字仅能出现在至多一个骰子上。

image

那都说了,最多两个数,那还能不是双段构造吗?

如果分成双段构造,那么不难看出:

早段1<早段2<<早段n\text{早段}_1 < \text{早段}_2 < \dots < \text{早段}_n,$\text{晚段}_1 < \text{晚段}_2 < \dots < \text{晚段}_n $(为了字典序最小,这两个条件是必须的),但是早晚段之间是什么关系呢?

注意啊,为了赢得游戏,早段n<晚段1\text{早段}_n < \text{晚段}_1,这个也是为了赢得游戏,满足所谓的“循环劣势要求”,所必须的这个构造,一个的最小的,大于一个的最大的,两者之间是绝对不会发生这个穿插的这个情况的,因此就是 早段1<早段2<<早段n<晚段1<晚段2<<晚段n\text{早段}_1 < \text{早段}_2 < \dots < \text{早段}_n < \text{晚段}_1 < \text{晚段}_2 < \dots < \text{晚段}_n

okay,那这道题目比较关键的部分就做完了,当然,还需要处理一些细节问题,就是每个段的早段是几个数,晚段是几个数?

不过贪心求解的部分也不是那么容易的。

首先,先写出一系列的这个获胜条件:

image

接着,我们要进行这个贪心求解,我们不难发现:

image

然后我们知道了 x1 的值,后面就用前面的限制,一个一个往后推就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void gen_eraly_seg() {
for (int i = 1; i <= N; ++i) {
ll early_sz;
if (i == 1) {
early_sz = K - (bound_win + K - 1) / K;
} else {
if (early_sz_ls[i - 1] - K == 0) {
early_sz = K;
} else {
early_sz = (bound_win - K * K) / (early_sz_ls[i - 1] - K);
}
}
early_sz = min(early_sz, K);
early_sz_ls[i] = early_sz;
for (int j = 1; j <= early_sz; ++j) {
ans_mat[i][j] = idx;
}
if (early_sz != 0) {
++idx;
}
}
}

void gen_late_seg() {
for (int i = 1; i <= N; ++i) {
for (int j = early_sz_ls[i] + 1; j <= K; ++j) {
ans_mat[i][j] = idx;
}
if (early_sz_ls[i] != K) {
++idx;
}
}
}

AC代码

AC
https://acm.hdu.edu.cn/contest/view-code?cid=1199&rid=17137

心路历程(WA,TLE,MLE……)

除零错误——INTEGER_DIVIDE_BY_ZERO

题目大意

题目描述
你有三个栈 A,B,CA,B,C 和两个变量 L,RL,R(相当于两只手)。栈中元素必须时刻满足:越靠近栈底的元素越大。

初始时,L=R=0L=R=0AA 栈中有 nn 个元素(从栈底到栈顶依次为 n,n1,,1n, n-1, \dots, 1),B,CB, C 栈为空。目标是将所有元素移动到栈 CC,且操作过程中必须满足上述大小限制。

可以进行两种操作:

  • 操作 1(拿起):选择一个非空栈 SS 和一个值为 00 的变量 VVLLRR),弹出 SS 的栈顶元素,并将该元素的值赋给 VV

  • 操作 2(放下):选择一个值为 x0x \neq 0 的变量 VV 和一个栈 SS,将 xx 压入栈 SS,并将 VV 的值设为 00

求:将所有元素移到栈 CC 所需的最少操作 1(拿起)的次数
结果需要对 998244353998244353 取模。

输入格式
第一行输入一个整数 TT1T1001 \le T \le 100)表示测试数据组数。
接下来 TT 行,每行输入一个整数 nn1n1061 \le n \le 10^6)。

输出格式
输出 TT 行,每行一个整数,表示最少使用操作 1 的次数对 998244353998244353 取模的结果。

样例输入

1
2
3
4
5
6
5
1
2
3
10
100

样例输出

1
2
3
4
5
1
2
4
36
268435446

样例解释(对于 n=3n=3
初始状态: A(3,2,1),B(),C(),L=0,R=0A(3,2,1),B(),C(),L=0,R=0
操作 1: A(3,2),B(),C(),L=1,R=0A(3,2),B(),C(),L=1,R=0
操作 2: A(3,2),B(1),C(),L=0,R=0A(3,2),B(1),C(),L=0,R=0
操作 1: A(3),B(1),C(),L=2,R=0A(3),B(1),C(),L=2,R=0
操作 1: A(),B(1),C(),L=2,R=3A(),B(1),C(),L=2,R=3
操作 2: A(),B(1),C(3),L=2,R=0A(),B(1),C(3),L=2,R=0
操作 2: A(),B(1),C(3,2),L=0,R=0A(),B(1),C(3,2),L=0,R=0
操作 1: A(),B(),C(3,2),L=1,R=0A(),B(),C(3,2),L=1,R=0
操作 2: A(),B(),C(3,2,1),L=0,R=0A(),B(),C(3,2,1),L=0,R=0
共使用了 4 次操作 1,因此答案为 4。

思路讲解

PDF

直接打表是没什么思路的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1:1
2:2
3:4
4:6
5:9
6:13
7:17
8:22
9:28
10:36
11:44
12:54
13:66

再大,时间复杂度就要求太大了,没法继续打了。我们一般看规律是用差分与二阶差分这样子去看的,不过这个难度还是比较大,想把这个规律看出来,主要是

不过,汉诺塔问题嘛,就是 3 步

image

当然,由于我们现在有两只手,所以情况稍微复杂一点,就是我们在没有辅助杆的情况下我们可以移动的盘子就不一定是 1 个了

image

当然,具体是几根也不是那么重要了。可以使用 bfs 求解,或者你对自己的直觉和数学功底比较自信的话,也可以自己整一整。

1
2
3
4
5
6
7
8
9
没有辅助柱子的情况下,就靠两只手,所需要的拿去次数
_______________[ Sample Testcase #1 OUTPUT]_______________
1:1
2:2
3:5
4:10
5:2123213341(随便设置的哨兵值)
6:2123213341

其实套路和经典汉诺塔问题一样。

image

AC代码

AC
https://acm.hdu.edu.cn/contest/view-code?cid=1199&rid=18423

心路历程(WA,TLE,MLE……)

打表的时候遇到的问题

小心,不要把这个 step 也用于判重,如果 step 要用于优先队列,那么就使用两个比较器

初始化顺序问题,栈的初始化顺序有可能是反的