写在前面:
省赛选手,省银(邀请铜尾),前面猛猛掉分,最后的 GG 还一直没开出来,图论想到死都没开出来,结果赛后一看是 DPDP

省赛牛魔不配有和牌的合照

补题连接


# A

# 题目大意

(a+b+c)d(a+b+c) * d

# 题解

C++
1
2
3
4
5
void solve () {
int a, b, c, d;
cin >> a >> b >> c >> d;
cout << (a + b + c) * d << endl;
}

# D

# 题目大意

现在有一个长方体,两个顶点分别位于 (0,0,0)(0,0,0)(a,b,c)(a,b,c)
nn 藤蔓,分别从 (x1,y1,z1)(x_1,y_1,z_1)(x2,y2,z2)(x_2,y_2,z_2)
你现在可以发动一攻击,任意从垂直 x,y,zx,y,z 轴的任意位置发动,切断与这一个截面相交的所有藤蔓
请问你最多切断多少条藤蔓?
(只需要考虑长方体内即可)

# 数据范围

  • 1n1051 \le n \le 10^5
  • 1a,b,c1091 \le a,b,c \le 10^9

# 题解

这题其实可以考虑成,在 x,y,zx,y,z 三个方向上的, 最多重复字段数 ,取最大即可

# 写法 1 - 贪心

这是我们队赛时的写法,利用贪心 + 优先队列,跑三遍​​最多重叠的区间数量
注意要按左端点排序( while 条件里是 heap.top() < v[i].first

C++
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
int n, a, b, c;

bool cmp(PII x, PII y) {
return x.first < y.first;
}

int getRes(vector<PII>& v) {
sort(v.begin(), v.end(), cmp);
int res = 0;

priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < v.size(); i++) {
int t = 0;
while (heap.size() && heap.top() < v[i].first)
heap.pop();
heap.push(v[i].second);
res = max(res, int(heap.size()));
}

return res;
}

void solve () {
cin >> n >> a >> b >> c;
vector<PII> x, y, z;
for (int i = 1; i <= n; i++) {
int x1, y1, z1, x2, y2, z2;
cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
if (x1 >= x2) swap(x1, x2);
if (y1 >= y2) swap(y1, y2);
if (z1 >= z2) swap(z1, z2);

x.push_back({max(0, x1), min(a, x2)});
y.push_back({max(0, y1), min(b, y2)});
z.push_back({max(0, z1), min(c, z2)});
}

cout << max({getRes(x), getRes(y), getRes(z)}) << endl;
}

# 写法 2 - 离散化差分

利用 map 自动排序的特性,进行离散化差分

C++
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
void solve () {
map<int, int> mp;

cin >> n >> a >> b >> c;
mp[a] = mp[b] = mp[c] = 0;

vector<array<int, 6>> t(n + 1);

for (int i = 1; i <= n; i++) {
for (auto& tt : t[i])
cin >> tt;
auto &[x1, y1, z1, x2, y2, z2] = t[i];

if (x1 >= x2) swap(x1, x2);
if (y1 >= y2) swap(y1, y2);
if (z1 >= z2) swap(z1, z2);

mp[x1] = mp[x2] = mp[y1] = mp[y2] = mp[z1] = mp[z2] = 0;
}

int idx = 0;
for (auto& [t1, t2] : mp)
t2 = ++idx;

vector<int> cfx(idx + 2), cfy(idx + 2), cfz(idx + 2);

for (int i = 1; i <= n; i++) {
auto &[x1, y1, z1, x2, y2, z2] = t[i];
cfx[mp[x1]]++, cfx[mp[x2] + 1]--;
cfy[mp[y1]]++, cfy[mp[y2] + 1]--;
cfz[mp[z1]]++, cfz[mp[z2] + 1]--;
}

int res = 0;
for (int i = 1; i <= idx; i++) {
cfx[i] += cfx[i - 1], cfy[i] += cfy[i - 1], cfz[i] += cfz[i - 1];
res = max({res, cfx[i], cfy[i], cfz[i]});
}

cout << res << endl;
}

# M

# 题目大意

现在小 AA 和小 BB 在玩抛硬币游戏,一共有 nn 枚最初都是正面朝上的硬币。现在他们有一个数字 kk,小 AAkk 枚硬币翻转成反面朝上了,但是小 BB 不知道有哪些硬币是被翻转了的,小 BB 需要对每枚硬币执行下面四种操作中的一种

  1. 放入第一堆
  2. 翻转后放入第一堆
  3. 放入第二堆
  4. 翻转后放入第二堆

若把所有硬币放完后,两堆硬币中,正面朝上的数量相同,则小 BB 赢游戏(允许某一堆硬币数为 00
你现在要输出一个操作序列,使得对于小 AA 的任意翻转顺序,小 BB 都能赢

# 数据范围

  • 1n1041 \le n \le 10^4

# 题解

题目最终是要两堆操作后正面向上硬币相同,不妨列出以下表格

第一堆第二堆
总数kknkn-k
选取到的反面个数xx(假设)kxk-x
选取到的正面硬币个数kxk-xn2k+xn-2*k+x

我们可以发现,此时,第一堆的选取到的正面硬币个数 = 第二堆的选取到的反面硬币个数,所以我们只需要,kk 个硬币选 1. 操作放入第一堆,nkn-k 个硬币选 4. 操作放入第二堆,即可实现题目要求

C++
1
2
3
4
5
6
7
8
9
void solve() {
int n, k;
cin >> n >> k;
for (int i = 1; i <= k; i++)
cout << 1;
for (int i = k; i < n; i++)
cout << 4;
cout << endl;
}

# F

# 题目大意

对于小 AA,在接下来的 nn 天中,若在第 ii 天摄入了 rir_i 卡路里,消耗了 cic_i 克质量,那么他的能量损失是

ci=p×ci1+(1p)×ri1c_i=p \times c_{i-1}+(1-p) \times r_{i-1}

其中,r0,c0,pr_0,c_0,p 已知
但是,小 AA 在这 nn 天中,有 kk 天的放纵日,这 kk 天的卡路里摄入量是预定的

除了放纵日的摄入,其余天的卡路里摄入量必须在 [L,R][L,R] 范围内
请你安排不是放纵日的日子,使得接下来 nn 天的总能量损失最大,即 maxi=1n(ciri)max \sum\limits_{i=1}^n(c_i-r_i)

# 输入格式

第一行包含两个整数 nnkk,表示未来有 nn 天,kk 天有预定摄入量
第二行包含五个实数 r0,c0,p,L,Rr_0,c_0,p,L,R
接下来 kk 行,每行有一个整数 pip_i 和一个实数 viv_i,表示 rpir_{p_i}viv_i

# 数据范围

  • 1kn1051 \le k \le n \le 10^5
  • 1n2×1051 \le n \le 2 \times 10^5
  • 1Lr0R1091 \le L \le r_0 \le R \le 10^9
  • 0<p<10 < p < 1
  • 1c01×1091 \le c_0 \le 1 \times 10^9

# 题解

根据题目给出的公式有

ciri=p×ci1+(1p)×ri1ric_i-r_i=p \times c_{i-1}+(1-p) \times r_{i-1}-r_i

求和有

i=1n(ciri)=i=1n(p×ci1+(1p)×ri1ri)\sum\limits_{i=1}^{n}{(c_i-r_i)}=\sum\limits_{i=1}^{n}{(p \times c_{i-1} + (1-p) \times r_{i-1} - r_i)}

展开有

i=1n(ciri)=p×i=1n(ci1ri1)+i=1nri1i=1nri\sum\limits_{i=1}^{n}{(c_i-r_i)}=p \times \sum\limits_{i=1}^{n}{(c_{i-1}-r_{i-1})}+ \sum\limits_{i=1}^{n}{r_{i-1}}-\sum\limits_{i=1}^{n}{r_i}

移项并合并同类项有

(1p)×i=1n(ciri)=[p×c0+(1p)×r0][p×cn(1p)×rn]=c1cn+1(1-p) \times \sum\limits_{i=1}^{n}{(c_i-r_i)}= [p\times c_0 + (1-p) \times r_0] - [p \times c_n-(1-p) \times r_n]=c_1 - c_{n+1}

i=1n(ciri)=c1cn+11p\sum\limits_{i=1}^{n}{(c_i-r_i)} = \frac{c_1-c_{n+1}}{1-p}

因为答案是求 maxi=1n(ciri)max \sum\limits_{i=1}^n(c_i-r_i),所以我们只需要让 cn+1c_{n+1} 最小即可,又因为

ci=p×ci1+(1p)×ri1c_i=p \times c_{i-1} + (1-p) \times r_{i-1}

故我们每次只当没有指定的时候,让 ri1r_{i-1} 取最小值 LL 即可

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
double r0, c0, p, L, R, day, v;

void solve () {
int n, k;
cin >> n >> k;
cin >> r0 >> c0 >> p >> L >> R;

map<int, double> mp;
for (int i = 0; i < k; i++) {
cin >> day >> v; // 表示第 day 天摄入了 v 卡路里
mp[day] = v;
}

double c1 = p * c0 + (1 - p) * r0;
double cn = c1;
for (int i = 2; i <= n + 1; i++) {
if (!mp.count(i - 1))
cn = p * cn + (1 - p) * L;
else
cn = p * cn + (1 - p) * mp[i - 1];
}

printf("%.10lf\n", (c1 - cn) / (1 - p));
}

# K

# 题目大意

你有 nn 个箭头,箭头可以朝向 ↑ ↓ ← → 四个方向,你有两种操作

  • 选中一个箭头,选中的箭头方向不变,其他箭头顺时针旋转 90°90°
  • 所有箭头顺时针旋转 90°90°

给定各个箭头初始时的朝向,如果要使所有箭头朝向变为 ,最少需要操作多少次?

# 数据范围

  • 1n1061 \le n \le 10^6

# 题解

初始方向第 ii 个指向 aia_i
操作 1. 相当于,当前这个选中的第 ii 的箭头逆时针旋转,即 -1 操作,但进行一次操作 11,会给全局带来一个 +1 的偏移量,所以我们可以暴力计算四个方向 0 1 2 3 ,先统一 nn 个箭头的方向,记操作数为 bib_i
此时,nn 个箭头的绝对指向是 (i+bi)%4(i + b_i) \% 4,所以我们只需要进行 (4 - (i + b[i]) % 4) % 42. 操作,即可将所有箭头都转至 00
故,最终答案是 b[i] + (4 - (i + b[i]) % 4) % 4

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void solve () {
cin >> n;
vector<int> a(n), b(4);

for (auto& t : a)
cin >> t;

for (int i = 0; i < n; i++)
for (int j = 0; j < 4; j++)
b[j] += (a[i] - j + 4) % 4;

int res = 1e18;
for (int i = 0; i < 4; i++)
res = min(res, b[i] + (4 - (b[i] + i) % 4) % 4);

cout << res << endl;
}

写题目的时候,要注重相对整体系统的概念,比如此题中的操作 11,它会使得选中的那个箭头,相对全局 1-1,但是不要忽略了,他对系统整体方向 +1+1 的影响,最终考虑方向的时候要把这个 +1+1 考虑进去

以上五题是赛时开的题目,下面是赛后补的题


# G

# 题目大意

有一个 nn 个点 mm 条单向边的图
一开始,你有体力 xx,当你通过一条长为 dd 的边时,你的体力会变成 xd\lfloor{\frac{x}{d}}\rfloor,当你的体力变为 00 的时候,代表体力耗尽,你将不再继续探索
现在有 QQ 个问题,对于第 ii 个问题,请问,若从 pip_i 点开始,初始体力为 xx,若想让体力耗尽,则最少要走多少条边?

# 数据范围

  • 1n,Q2×1051 \le n, Q \le 2 \times 10^5
  • nm5×105n \le m \le 5 \times 10^5
  • 1u,vn,2d1091 \le u, v \le n, 2 \le d \le 10^9
  • 1x1091 \le x \le 10^9

# 题解

这个题目的核心是,把走若干条边的除法转换为了 d1×d2×...×d...>xd_1 \times d_2 \times ... \times d_{...} > x

定义 DPDPdp[i][j] 表示从 jj 点出发,经过 ii 步后,消耗乘积的最大值

dp[i][u]=max{duv1×...×dvi1vi}dp[i][u] = max\{d_{u \to v_1} \times ... \times d_{v_{i-1} \to v_{i}}\}

那么状态转移方程为

dp[i][u]=max(uv)E(dp[i1][v]×duv)dp[i][u] = \max_{(u \to v) \in E} \left( dp[i - 1][v] \times d_{u \to v} \right)

  • xx 最大值是 10910^9,在 3030 次内便可以除完

C++
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
int n, m, q, p, x;
vector<PII> g[N];

void solve () {
cin >> n >> m >> q;
for (int i = 1, u, v, w; i <= m; i++)
cin >> u >> v >> w, g[u].push_back({v, w});

vector<vector<int>> dp(31, vector<int>(n + 1, 1));
for (int i = 1; i <= 30; i++) {
vector<int> tmp(n + 1, 1);

for (int u = 1; u <= n; u++) {
// 最大是 1e9,也就是说,如果超出了 1e9 就没必要更新了,这是小小的优化
// if (dp[i - 1][u] >= 1e9)
// continue;

for (auto &[v, w] : g[u])
tmp[u] = max(tmp[u], dp[i - 1][v] * w);
}

dp[i] = tmp;
}

while (q--) {
cin >> p >> x;

for (int i = 1; i <= 30; i++)
if (dp[i][p] > x) {
cout << i << endl;
break;
}
}
}

对于这种超大数据范围的图论题、最短路 / DFSDFS / BFSBFS 走不通的题目
其实基本上不用考虑图论里的算法了,可以考虑下 DPDP,特别是这种走若干步的,基本上不太可能 DFSDFS / BFSBFS

如果题目出现连除下取整,考虑转换为乘法 + DPDP 去做

# I

# 题目大意

我一天有 nn 个时间段,一个字符串 ss 描述我的日程安排

  • si=1s_i = 1 表示我在 ii 时间段很忙
  • si=0s_i = 0 表示我在 ii 时间段有空

现在我必须执行一次这个操作:

  • 选择一个范围 [l,r][l, r]lrl \to rkk 个时间段很忙(kk 是一个确定的常数)
  • 选择后,任意修改 lrl \to r 的忙 / 闲状态,但是你需要保证的是,修改后 lrl \to r 中,忙的时间段的数量和修改前是一样的

请问,我最多可以得到多少个不同的时间表?答案很大,需模 998244353998244353

# 数据范围

  • 1n1051 \le n \le 10^5

# 题解

需要预处理的数据

  1. fac[i] :表示 ii 的阶层
  2. inv[i] :表示 ii 的阶层的逆元
  3. suf[i] :表示 ii 位置后连续的 00 的长度

因为操作只能做一次,我们可以用双指针滑动窗口找满足 11 的个数恰好是 kk 的区间
对于一个合法的操作窗口,我们的答案累加上

C(rl+1)+sufikC_{(r - l + 1) + \text{suf}_i}^{k}

代表从 窗口 + 窗口后连续的 1 进行排列组合,选取 kk 个位置放 11
用一个 flagflag 标记是否是第一次操作滑动窗口,如果不是第一次操作滑动窗口,则还需要去重,需要减去

Crlk1C_{r-l}^{k-1}

  • 为什么要减?
    • 因为 新窗口 = 旧窗口 + 下面新延伸到的第一个 1
    • 而对于保持新延伸到的 11 不变的方案,是之前重复了的
    • 所以需要减去,最后一个 11 确定放在最后一个位置,前面的 k1k - 111rlr-l 区间中随机放的方案数

C++
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
84
85
86
87
88
89
90
91
92
93
94
95
const int N = 1e5 + 5, MOD = 998244353;

// fac[i] 是 i 的阶层,inv[i] 是 i! 的逆元
vector<int> fac(N), inv(N);

int qmi(int a, int b) {
int res = 1;

while (b) {
if (b & 1)
res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}

return res;
}

// 组合数 cal(n, k)
int cal(int n, int k) {
return fac[n] * inv[k] % MOD * inv[n - k] % MOD;
}

void solve() {
int n, k;
string s;
cin >> n >> k >> s;
s = " " + s;

// suf[i] = 从 i 往后连续的 0 的长度
vector<int> suf(n + 2, 0);
int cur = 0;

for (int i = n; i >= 1; i--) {
cur += (s[i] == '0');
suf[i] = cur;

if (s[i] == '1')
cur = 0;
}

int ans = 0, cnt = 0;
bool flag = false;

for (int l = 1, r = 1; r <= n; r++) {
if (s[r] == '1') {
cnt++;

while (l <= r && cnt > k) {
if (s[l] == '1')
cnt--;
l++;
}

if (cnt == k) {
// [l..r] 最小窗口里正好 k 个 1,
// 把所有以 r 开始后面接 0 的区间一次性加上:
// 长度 = (r-l+1) + suf[r]
ans = (ans + cal((r - l + 1) + suf[r], k)) % MOD;

// 从第二个“合法窗口”开始,每次减掉 cal(r-l, k-1) 去重
if (!flag)
flag = true;
else
ans = (ans - cal(r - l, k - 1) + MOD) % MOD;
}
}
}

if (k == 0)
ans = 1;

cout << ans << endl;
}

signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);

fac[0] = 1;
for (int i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % MOD;

inv[N - 1] = qmi(fac[N - 1], MOD - 2);
for (int i = N - 2; i >= 0; i--)
inv[i] = inv[i + 1] * (i + 1) % MOD;

int _ = 1;
cin >> _;

while (_--)
solve();

return 0;
}

# E

# 题目大意

定义一个kk 折叠字符串的概念

  • 当且仅当存在一个字符串 tt,使得 s=tks = t^k,即 tt 重复连续 kk 次恰好形成 ss

对于给定的字符串 SS,每个查询给出正整数 xxyy,请问有多少对正整数 l,rl, r 满足

  • xl<ryx \le l < r \le y
  • S[l,R]S_{[l, R]} 可以重排成 kk 折叠字符串

你需要回答 qq 个查询

# 数据范围

  • 1n,k,q3×1051 \le n, k, q \le 3 \times 10^5
  • 时间限制 3seconds3~seconds

# 题解

重排字符串很容易能理解成,对于 S[l,r]S_{[l, r]},其内部所有字符的出现次数,都能被 kk 整除

我们统计一个 vector<int> cnt(26) 计算各个位置上的字符个数(均为对 kk 取模后的值)
那么,如果遍历到 ii 时候计算得到的 cnticnt_i 和遍历到 jj 得到的 cntjcnt_j 数组完全相同的话(i<ji < j),那么即说明,S[i+1,j]S_{[i+1, j]}kk 折叠字符串
可以把第 ii 个位置的 vector<int> cnt(26) 哈希至 hash[i]

所以原问题被转换为,对于每个查询 (x,y)(x,y),能找到多少对 (i,j)(i, j) 满足

  • x1i<jyx - 1 \le i < j \le y
  • hash[i] == hash[j]

为了方便描述解释,此处先给出代码

cpp
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
int n, k, q, idx, res;
string s;
vector<int> pos(N);
vector<int> c;

struct Md {
int l, r, id;
};

void add(int x) {
res += c[x];
c[x]++;
}

void sub(int x) {
c[x]--;
res -= c[x];
}

void solve() {
cin >> n >> k >> q;
cin >> s, s = " " + s;
int len = sqrt(n);

vector<int> cnt(26, 0);
map<vector<int>, int> mp;
vector<int> hash(n + 1);
mp[cnt] = 0;

for (int i = 1; i <= n; i++) {
cnt[s[i] - 'a']++, cnt[s[i] - 'a'] %= k;

if (!mp.count(cnt))
mp[cnt] = ++idx;

hash[i] = mp[cnt], pos[i] = i / len;
}

c = vector<int>(idx + 1, 0);

vector<Md> md(q + 1);
for (int i = 1; i <= q; i++)
cin >> md[i].l >> md[i].r, md[i].id = i;

// 按所在块和右端点排序
sort(md.begin() + 1, md.begin() + q + 1, [](const Md &a, const Md &b) {
return pos[a.l] == pos[b.l] ? a.r < b.r : pos[a.l] < pos[b.l];
});



// 答案数组
vector<int> ans(q + 1);

// 维护已知区间的左端点和右端点
int cl = 0, cr = 0;
add(hash[0]);

for (int i = 1; i <= q; i++) {
auto &que = md[i];
int l = que.l - 1, r = que.r, id = que.id;
cout << "l: " << l << ", r: " << r << ", id: " << id << endl;
while (cl > l) add(hash[--cl]);
while (cr < r) add(hash[++cr]);
while (cl < l) sub(hash[cl++]);
while (cr > r) sub(hash[cr--]);

ans[id] = res;
}

for (int i = 1; i <= q; i++)
cout << ans[i] << endl;
}

  1. 如何理解两个关键函数 addsub ?
    C++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void add(int x) {
    res += c[x];
    c[x]++;
    }

    void sub(int x) {
    c[x]--;
    res -= c[x];
    }

    分别以右边加一个值左边减一个值为例,进行说明
  • 我们的目的是找对于给定的 [x,y][x, y] 中,能找到多少个 (i,j)(i,j) 满足 x1i<jyx-1 \le i < j \le y 并且 hash[i] == hash[j]
  • 直接暴力计算是肯定不行的,我们可以考虑利用来计数,用 c[hash[t]]c[hash[t]] 计算已有的 hash[t]hash[t] 出现次数,那么当新出现一个 hash[t]hash[t] 的时候,它可以和前面已经有的 hash[t]hash[t] 组成 kk 折叠字符串,也就是可以加上 c[hash[t]]c[hash[t]]
  • 而在莫队的过程中
    • 当右边加上一个值时(假计下标为 tt),也就是说,加上 tt 位置后,多了一个 hash[t] ,这个新的 hash[t] 可以和前面已经有的 hash[t] 组成 kk 折叠序列,所以应该先让 resres 加上已有的 c[hash[t]] 再让 c[hash[t]]++
    • 当左边减去一个值时(假计下标为 tt),也就是说,减去 tt 位置后,少了一个 hash[t] ,而借鉴前面的加上一个值的过程,实际上 kk 折叠字符串其实是少了 c[hash[t]] - 1 个,所以我们先让 c[hash[t]]-- ,再让答案 resres 减去 c[hash[t]]
  1. 为什么要先 add(hash[0]) ?
    因为我们查询的是在区间 [x - 1, y] 中,这样的 (i, j) 对有多少个满足 i < j 并且 hash[i] == hash[j] ,那么一开始 hash[0] 也要被加入进去,因为 cnt{0, 0, ..., 0} 对于后面 2626 个字母全部都出现 ikik 次,实际上是一个前缀,也就是说 hash[0] 要被第一个加入,即使可能很少被运算到