主线任务:上 2100,支线任务:成为一名优秀的代码攻击者。

最近感觉自己又行了。就很想重新开始打 Codeforces,这里罗列一下从 2020/05/16 开始提交的(不是特别水的)AC 代码。想有生之年上一次黄,先定个小目标上个 1900(最高 1854,当前 1829,换号了!新账号于 2020/11/18 达成 1900+ 成就,紫名:)也太好看了吧)菜鸡 dna049 加油 下班了!

不再打除了 codeforces, atcoder, 洛谷之外的日常比赛。有空整理一下比赛,好的题目或比赛记得 star。
优雅的代码极大的避免了低级错误,边界处理至少占三分之一的工作量!

不要急着写代码,不要急着写代码,不要急着写代码,重要的事情说三遍!!!
优先做 Codeforces, Atcoder, 其它平台的题目可以在 vjudge 中提交(洛谷和 UOJ 除外),POJ 和 HDU 不在搞了。
不要为了补题,Rating,浪费太多的时间
把一个问题写成简洁清晰的代码,才算是真的理解了问题!代码能力是快速综合规划的能力。
至少先上 2100,再去写介绍性博客,分低会影响博客的质量!至少上 2100 才配出题
不再补比赛中没有考虑过的题

准备工作

珍惜每一场比赛,不要怕掉分(怕的原因就是不自信),提前 8 到 10 分钟,准备以下事情(加号表示有用次数,2021 年开始计算):

  1. 如果没注册,赶紧注册。身体状态不好时,用小号打,上分不易。+2
  2. 闹钟(每个题不要长期思考,读完题没明显思路直接跳!只要不被卡题,我上 2100 应该不难,所以闹钟很重要) +2
  3. 纸和笔(ipad + pencil + 闹钟 亦可)+0(表示:永远有用)
  4. SageMath,用于一些数据测试
  5. 打开自己的网站用于模板
  6. 打开 VScode,查看 CF 比赛号,开始 cf race CF 比赛号 +0
  7. 认真读题,特别是样例特别少的时候!
  8. 计算式要列的清晰,并且最好是好输入的(think twice, code once)。+3
  9. 不要膨胀,老老实实先做简单题,没思路就直接跳过(拒绝拖延,但是一定记得要回来把简单题补了!) +1
  10. 有思路赶紧写(但是思路清晰再写!无论问题难与否,大家都是一样的,打好自己!),不要觉得时间很多
  11. 最后 15 分钟(或者觉得没题能做),如果没有思路就不要再做题了,攻击代码去。开 room hack!锁题前检查边界,不要最后 RE 了(根据做题时,可能的 hack 点来找 hack,可以现在 EDU 场练习如何 hack。比赛结束看看自己 room 有那些没有过最终测试的。通用 hack 点:答案超了 long long,用 ceil 向上取整。注意有些狗喜欢 #define int long long
  12. 前面题目被卡会特别影响做后面题的心态!
  13. 注意到题目中简单的性质结论都应该写下来,拼一下可能就知道怎么做了。注意特殊问题的特殊解法。
  14. WA 之后,很可能方法错了,或者代码有问题,没有本质修改的话,不要乱提交!
  15. 有子问题的题目,两个都会做,先交分数小的。
  16. 不要写这样的代码if(a[j++]), while(a[j++]) 这种数列下标自加,特别容易出问题还不好查,特别在判断语句中。
  17. $N=10^{18}$ 一般会有公式, $N = 2 \cdot 10^5$ 一般会用到排序、树状数组,线段树,等一系列 $O(n \log n)$ 算法,$N=1000$ 一般会用到 $dp$。模 998244353 可能会用到 NTT。

Codeforces 比赛规则:每题有基础分数,每题的分数根据时间线性递减到一半(所以有些大佬先做 CD,再做 AB),在最后一次提交前每次提交减 50 分,然后按照分数排名,有 hack,但是只有你过了初例并且 锁定题目 才能 hack。
Eductional 比赛规则:与 ICPC 一致,不过一次非 AC 提交罚时为 10 分钟(ICPC 20 分钟),难度比 Div2 稍难,需要更细心。(+10-184+69-184-20-21-77-36+15-11)最近不再打正式 Edu 场(2021-4-25)+++。

Codeforces Gobal Round:目前实力不支持做这个,容易掉分! 正常发挥都是能加分的!

查看 Codeforces 上的过题数查看比赛 rating 折线,开始使用 cf-tool + WSL 打比赛(真香)。

题集

Deltix Round, Spring 2021 (open for everyone, rated, Div. 1 + Div. 2)

A 题题目理解错误被卡了一下,怕掉分就没敢做,其实只要做出 D 就可以加分,我当时不自信

A:注意到最多 n 次就开始稳定了

B:只考虑两个的情况就可以了

C:直接从头开始,然后找可能即可

我一开始以为要从后面开始找,浪费了点时间

D:一看这题就要用 mask,但是没想到是概率 + mask

jiangly 代码还学了一手卡时间技巧

这个 mak 的处理特别细节 $p \cdots 2^p$,值得学习,当然也可以直接暴力 $3^p$。

E:组合概率问题

有连续 n 个灯,如果一个长度为 k 的区间包含大于两个亮的灯就停止工作,问停止工作时亮灯的个数的期望

所以假设现在有 p 个亮灯但是没有停止工作,这等价于

所以种类是 $\binom{n - (p - 1)(k - 1)}{p}$

虽然这不是最终停止工作时的答案,但是它贡献了答案

武汉理工 2021 同步赛 F

官方题解我的提交

我一开始一直 TLE 原来是因为思路有问题,注意代码中求 sumPhi 会有很多公共项

注意到 $x = \min(\lfloor \frac{n}{i} \rfloor, \lfloor \frac{m}{j} \rfloor), gcd(i, j) = 1$ 等价于 $\gcd(xi, xj) = x$ 的最大的 $x$

这么说来这也是给出了更快做法(前提 $f(0) = 0$)

Codeforces Round #723 (Div. 2):题目很不错,可惜我掉分了

A:排序交错取值即可

B:你会发现只有 11 和 111 是有用的

C:这题我应该 C1, C2 一起做的

首先我们肯定会取所有非负的,然后尽量取最多,如果不能取,那么我们把最小的负数换掉即可。

用 优先队列比用 multiset 快

D:这题如果注意到最优策略必然是 AAAABBBCCCDDD 的样子,那么直接全排列处理一下即可

其实我一看到这题就知道要全排列,但是!!!我思考的中途忘了!

用 树状数组求逆序数或者直接暴力求都可以。

E:组合计数

首先,我们看计数非 0 的最小 k 为多少。考虑两个相邻(按照大小排序后)的后缀 $ax$ 和 $by$,若 $x > y$ 那么 $a < b$,否则 $a \leq b$。所以我们要看有多少种 $a \leq b$ 的可能,这样就可以知道最小需要多少个 k。假设有 e 个 相等。那么最小的 k 为 $n - e$。现在问题怎么计数,其实它等价于求 $a_1 + \cdots + a_n \leq k - (n - e)$ 的非负整数解的个数,答案显然是 $\binom{k + e}{n}$(先补成等于号,再插空法)

896E:第二分块模板

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

// 第二分块,如果值域很离谱是不是可以直接用 map 呀!就多一个 log 操作,但是特别容易 TLE
class BlockMinus {
std::vector<int> fa, sz, a;
int l, delta, mx; // 真实值为 x - delta
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) { // merge x to y
x = find(x); y = find(y);
if (x == y) return;
fa[x] = y;
sz[y] += sz[x];
sz[x] = 0;
}
void modifyPart(int ql, int qr, int x) {
for (int i = ql; i < qr; ++i) {
a[i] = find(a[i]);
if (a[i] - delta > x) {
--sz[a[i]];
a[i] = find(a[i] - x);
++sz[a[i]];
}
}
}
void modifyAll(int x) {
if (x < mx - delta - x) {
for (int i = delta + 1; i <= x + delta; ++i) merge(i, i + x);
delta += x;
} else {
for (int i = mx; i > x + delta; --i) merge(i, i - x);
mx = x + delta;
}
}
int queryPart(int ql, int qr, int x) {
int ans = 0;
for (int i = ql; i < qr; ++i) {
if (find(a[i]) - delta == x) ++ans;
}
return ans;
}
int queryAll(int x) {
x += delta;
if (x > mx || find(x) != x) return 0;
return sz[x];
}
public:
// 这里不写构造函数比较好
void init(const std::vector<int> &_a, int _l, int _r) {
l = _l, delta = 0;
a = {_a.begin() + _l, _a.begin() + _r};
mx = *std::max_element(a.begin(), a.end());
fa.resize(mx + 1); std::iota(fa.begin(), fa.end(), 0);
sz.resize(mx + 1); for (auto x : a) ++sz[x];
}
void modify(int ql, int qr, int x) {
if (x >= mx - delta) return;
if (qr - ql == (int)a.size()) modifyAll(x);
else modifyPart(ql - l, qr - l, x);
}
int query(int ql, int qr, int x) {
if (qr - ql == (int)a.size()) return queryAll(x);
return queryPart(ql - l, qr - l, x);
}
};

int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, m;
std::cin >> n >> m;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
// 可以离线公用同一个 BlockMinus 省空间!如果强制在线的话,空间就不知道怎么省了
int sn = sqrt(n);
std::vector<BlockMinus> block;
for (int i = 0; i < n; i += sn) {
BlockMinus tmp;
tmp.init(a, i, std::min(i + sn, n));
block.emplace_back(tmp);
}
while (m--) {
int op, l, r, val;
std::cin >> op >> l >> r >> val;
--l;
int il = l / sn, ir = r / sn;
if (op == 1) {
if (il == ir) {
block[il].modify(l, r, val);
} else {
block[il].modify(l, il * sn + sn, val);
for (int i = il + 1; i < ir; ++i) {
block[i].modify(i * sn, i * sn + sn, val);
}
if (ir < block.size()) block[ir].modify(ir * sn, r, val);
}
} else {
int ans = 0;
if (il == ir) {
ans += block[il].query(l, r, val);
} else {
ans += block[il].query(l, il * sn + sn, val);
for (int i = il + 1; i < ir; ++i) {
ans += block[i].query(i * sn, i * sn + sn, val);
}
if (ir < block.size()) ans += block[ir].query(ir * sn, r, val);
}
std::cout << ans << '\n';
}
}
return 0;
}

The 16th Heilongjiang Provincial Collegiate Programming Contest

A:典型线段树

但是 and 线段树,不知道怎么处理,后来看别人才知道,每个点最多 and 有效 bit 次。

D:区间 DP

这题数据太水了,$O(n^3)$ 裸就过了

F:预处理最小素因子直接算即可

这题可以类似求 $\pi(x)$ 做到 $O(n^{\frac{2}{3}})$ 从而计算到 $O(n^12)$ 但是此时要用 int128 存结果可能会很慢,但是 $O(n^10)$ 问题不大。

H:假路径压缩

这题把方向搞反了 WA 了几次

I:能难倒我的 SG 游戏题

这不就是经典阶梯 SG 吗?我在干什么!这种问题之前还遇到过了!

J:纸老虎

注意到题目的限制就说明了最多仅有一个没有匹配,正是那个全部被搞了的点。否则就全能匹配

K:签到水题

注意到只要总和大于等于 K 块,就总能只留下 $\lfloor \frac{n}{2} \rfloor$

2021 Hubei Provincial Collegiate Programming Contest

A:异或纠错码

挺简单,但是还有有意义的一道题,反正就在最后面补 k 个 0,然后处理到最后,这 k 个字符就是答案。

D:不交区间维护

昨天 cf 722 div 1C 本质就是不交区间维护更新

很有意思的一题,注意到 C 必然是连续的一段自然数,我们先保存所有值的位置,然后我们从最小值开始遍历,用 Set 维护不交并的区间即可。

F:贪心

注意到 $b_i$ 是 $2$ 的幂次,因此我们可以让 $a$ 从小到大贪心的每次取最小,就不用担心大的因为先取了小的而没取到的问题。

J:用复数处理相似三角形

首先对一个图形进行伸缩和旋转是相似变换,那这不就相当于用一个复数乘以所有的端点吗?我们要最小的整格点,那么我们就除以它们的最大公约数即可,而 Euild 整环都可以用带余除法求最大公约数。注意这里我们做除法的时候要使得余数的模最小。

K:经过一轮取差取绝对值(经典分块)

根据和 Kelin 的 Talk,注意到值域是单调递减的。利用第二分块解决(为什么叫第二分块,有空可以挑战以下 Ynoi 分块)

最后根据 zimpha 的代码读懂了 第二分块 的做法。

然后取差之后绝对值也就相当于两次 第二分块 来持续处理一个区间。

注意到这题不像原始第二分块可以直接黑科技暴力过!

Codeforces Round #722 (Div. 1)

好高骛远导致又一次离 master 最近的一次

又是一次 dfs 序的应用!!!

A:绝对值最值问题

注意到最大值必然在边界取到,所以树上 DP 即可

B:找规律 DP

不难发现 $a_n = \sum_{i = 1}^{n - 1} a_i + d(n)$,所以整体复杂度 $O(n \log n)$

C:dp 序应用

这题我知道要用 dfs 序,我也知道 dfs 序的性质:$u$ 是 $v$ 的祖先,当且仅当 $in[u] \leq in[v] \leq out[v] \leq out[u]$,但是我们需要的是反过来,这样就很多了,所以我因为我做不了,然后就取读其它题,然后专注最后一题,未果。其实 dfs 还有一个基本的性质,不存在交叉的情况,即不存在 $u, v$ 满足 $in[u] \leq in[v], out[u] \leq out[v]$,所以如果它们不互为祖先关系,那么必然是两两不交的!

F:经典组合问题(Stirling 数和下降幂的应用)

题解一波对应操作,就把问题转化成求

注意到上述式子中 $j > k$ 时 $S(k, j) = 0$

2020-2021 Winter Petrozavodsk Camp, UPC contest:质量真的高

A:可以直接 DP

a[n][k], b[n][k] 分别为 $n$ 与 $n - 1$ 不相连,相连的答案,则
$a[n][k] = (k + 1) a[n - 1][k + 1] + (n - 2 - k) a[n - 1][k] + k b[n - 1][k + 1] + (n - 1 - k) b[n - 1][k]$
$b[n][k] = b[n - 1][k - 1] + b[n - 1][k] + a[n - 1][k - 1] * 2$

B:构造排列使得 $|a_i - i|$ 还是一个排列

首先根据奇偶性发现 $n \mod 4 = 0, 1$ 才有可能。打表找规律 取 $n = 4, 5, 8, 9, 12$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

template<typename T>
void debug(std::vector<T> a){
for (auto &i : a) std::cout << i << ' ';
std::cout << std::endl;
}

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<int> a(n);
std::iota(a.begin(), a.end(), 0);
do {
std::set<int> S;
for (int i = 0; i < n; ++i) S.insert(abs(a[i] - i));
if (S.size() == n) debug(a);
} while (std::next_permutation(a.begin(), a.end()));
return 0;
}

我们想前 k 个放 $n - 1, … n - k$, 后 k 个 放 $k + 1, \cdots, 1$, 然后把 0,放进去,再处理。

乘积图的最小生成树

乘积图的最小生成树也是贪心的做法:第一张图两个点相连,需要连第二张图剩下的点的个数,相当于此后第一张图的点就少了一个。那么利用 排序 + DSU 贪心即可。

I:思路简单却很坑的一题

首先解方程 $2x + y = a, 3x + y = b$ 显然解为 $x = b - a, y = a - 2x$。显然我们要求 $x \geq 0, y \geq 0$。然后考虑一下第一个人的特殊性:要求 $x >= n - 1$,最重要是:

平局需要至少两个人参与!

而就要求所有平局数为偶数,且平局数的最大值不能大于其它的和,并且只要这样就够了。这是因为我们每次取平局数最大的和平局数次大的打一场平局,那么此时依然满足这个性质!

还有就是,是路径而非右边相连,所以就需要赢的次数和大于 $n - 1$!还要特判 $n = 1, 2$ 的情况!

这题也太坑了,连题目作者都没有考虑平局有可能不行的情况!

J:简单素数问题

首先注意到 $(10 n + 5)^2 = 100 n(n + 1) + 25$,那么答案就是 $n$ 和 $n + 1$ 的互异素因子个数和。

L:找递推关系式

要特别细心才行!你会发现直接递推关系式长度与 n 有关。。所以我们要取多个递推关系来搞,分析下来有 8 种情况要考虑:
{, , }, {, 111, 111}, {, 1, 11}, {1, , 11}, {, 01, 11}, {, , 111}, {, 11, 1}, {1, 11, },我们用 $a_i$ 表示 $a_i(n)$,$a_i’$ 表示 $a_i(n - 1)$,那么
$a_0 = a_0’ + a_1’ + a_2’ + a_3’ + a_4’$,(通过观察右上角的那个快怎么来的,其它的观察最长的那个怎么来的)
$a_1 = a_2’ + a_5’ + a_6’$, $a_2 = a_0’ + a_7’$, $a_3 = a_2’$(这里用了一下对称), $a_4 = a_5 = a_0’$, $a_7 = a_8 = a_0’ + a_3’$(这里也应用了一次对称)。

因此这个问题可以优化成 6 种 case,以下是优化后的情况

使用 sageMath

1
2
3
4
5
6
7
8
9
10
A = matrix(6)
A[:, 0] = A[0, :] = A[1, 2] = A[1, 4] = A[1, 5] = A[2, 5] = A[3, 2] = A[5, 3] = 1
A[0, 5] = A[1, 0] = A[3, 0] = 0
b = matrix(6, 1)
b[0] = 1
for i in range(6):
print((A^i * b)[0])
A.characteristic_polynomial()
# charPoly(A) = x^6 - x^5 - 2*x^4 - 6*x^3 - x^2 + 1
# b[0], (Ab)[0],..., (A^5 b)[0] = (1, 1, 3, 10, 23, 62)

这个问题可以自动化吗?

AtCoder Beginner Contest 202

A:签到题

筛子问题

B:签到题

数字字符倒过来

C: 水题

利用 map,注意到 C 数列本质可变成 B 数列。

D:组合数序

$a$ 个 ‘a’,$b$ 个 ‘b’,注意到,如果第一个位置填 ‘a’,那么最多有 $\binom{a + b - 1}{b}$ 个数,如果 k 大于它,那么说明只能填 ‘b’ 此时 k 要减去这个数。

E:经典树上问题

这个问题我一开始一直想用倍增做,然后发现复杂度不对,但是想不到别的做法,于是我就开始倍增做,然后代码直接爆炸,最后 TLE。

再次发现 dfs 序的重要性!!!

Stanford ProCo 2021

A:水题

利用 $a, b \geq 0$ 时 $a + b \geq a \oplus b$

C:map

利用 map,然后 map 前缀和,再用一个 vector 保存 map 的值,再二分查找即可

E:找一个长为 n 的排列,最长递增为长度为 x,最长递减为 y

不妨假设 $x > y$,那么我们构造 $n - x + 1, \cdots, n$ 就可以让 n 变成 $n - x$, y 减一了。

H:田忌赛马

排序加 set 即可

J:签到题

K:考虑前缀与后缀,然后“相加”即可

L:经典问题:特殊的图的可达性问题

通过原图和反图分别求 “可达点” 的个数(这里并不是真的求了可达点)。我们从图的入度为 0 的点开始考虑,如果考虑到 $u$ 发现当前队列中还有点,就说明这个点不能到达它除了它祖先外的所有点,因此它不可能成为答案,那就不计算它的可达数,即使计算了也不一定正确(或者说不会大于自己的可达点个数)。

这个题好特殊啊,保证了能求由很难拓展!

N:水题

分奇偶即可

Codeforces Round #721 (Div. 2)

A

求最大的 $k$ 使得 $n \And (n−1) \And (n−2) \And … \And k = 0$,显然二进制 n 为 $1…$,不把首个 1 消了不可能为 0,所以答案就是 1 << std::__lg(n) - 1

因为 cf test 太卡导致 cf submit 时 00::02:01 气死了。我去改 cf-tool 设置!

B

B1 比较简单,一开始就是回文,此时如果 n 为偶数,后手必胜;若 n 为奇数,此时中心元素若是 1,那么就跟 n 为偶数没区别;否则中心元为 0, 那么先手必抢这一点,那么如果还有多余的 0,那么先手必赢,否则必输。

B2 如果一开始不是回文,那么先手想怎样就怎么样,等到只有一个地方不是回文的时候果断把那个位置一填,就基本赢了,唯一的可能和就是 n 为奇数中心元为 0,且只有两个 0

C

真的气,我以为是求逆序数,交了一把逆序数上去,吐了。后来发现是看相等的地方,那么直接 map 一下考虑贡献即可。

D

首先注意到 $Mex(S) > i$ 当且仅当 $S$ 中存在 $0, 1, 2, \cdots, i$。

不包含 $i$ 可以推出 $Mex(S) \leq i$ 但这不是充要的。

那么我们找路径中包含它们的值即可。因此我们可以让树的每个节点记录它的子孙中节点编号的最小值(并且把这个分支当作真儿子),且记录以它为子树的大小。

$Mex(S) = 0$ 当且仅当 $S$ 中不包含 $0$,那么就是 0 为根的树,每一个分支中任取两点再求和即可
$Mex(S) > 0$ 当且仅当去掉 $Mex(S) = 0$ 的情况
$Mex(S) > 1$ 当且仅当包含 1,那么答案就是 $sz[1] \cdot (n - sz[son[0]])$ 而非 $sz[1] \cdot (n - sz[1])$!因为 0 和 1 不一定直连啊!哎。
如果 i + 1 在 i 的子树中,那么就一直往下跑,答案为 $sz[i + 1] \cdot (n - sz[son[0])$,否则我们还有一次机会,这个时候 $i + 1$ 在另外一个分支上,在哪无所谓,答案变成了 $sz[i + 1] \cdot sz[i_0]$。再往下跑,不满足条件就结束。

最后 $|Mex(S) = i| = |Mex(S) > i - 1| - |Mex(S) > i|$

特别的生气!!! 我为什么弱智的把 $sz[1]$ 和 $sz[son[0]]$ 搞混淆了!!! 啊,好气啊!就差这么一点,这个题做出来直接能 +100 多,就能打破我的最佳排名记录了。后来我发现我想错的点不止一个,确实是自己实力不够。

AtCoder Regular Contest 119

A:NTT-friendly 数:枚举 b 即可

B:一开始以为是贪心

这个 评论 一下子顿悟了

C:这种问题之前遇到了,竟然一下子不知道怎么做

意识到区间可行当且仅当它们的交错和(alternating sum)为 0,就简单了。

注意交错和有 n + 1 个值。

Educational Codeforces Round 109 (Rated for Div. 2):血的教训最近别打 EDU 场

A:送分诱导参赛上当

B:要考虑到头为 n 尾部为 1 的情况

C:感觉挺麻烦就去做 D 去了

首先注意到每次奇数都会变成偶数,偶数变成奇数,就分两种情况处理即可。然后排序,一左一右的配对,用两个堆保存当前情况。最后堆非空只会出现 L..LR..R 的情况,然后相邻同样的两类配对,反射可以看出 $x$ 变成 $-x$ 或 $2m - x$。最后可能会变成 $LR$,$L$,$R$,空四种情况。看 size 讨论一下即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, m;
std::cin >> n >> m;
std::vector<int> a(n), id(n);
std::string s(n, '0');
for (auto &x : a) std::cin >> x;
for (auto &c : s) std::cin >> c;
std::iota(id.begin(), id.end(), 0);
std::sort(id.begin(), id.end(), [&](int i, int j) {
return a[i] < a[j];
});
std::vector<int> A[2], ans(n);
for (auto x : id) {
int bit = a[x] & 1;
if (s[x] == 'R') A[bit].emplace_back(x);
else if (!A[bit].empty() && s[A[bit].back()] == 'R') {
ans[A[bit].back()] = ans[x] = (a[x] - a[A[bit].back()]) / 2;
A[bit].pop_back();
} else A[bit].emplace_back(x);
}
for (int bit = 0; bit < 2; ++bit) {
while ((int)A[bit].size() > 1) {
int x = A[bit].back();
A[bit].pop_back();
if (s[x] == 'R' && s[A[bit].back()] == 'R') {
ans[x] = ans[A[bit].back()] = m - (a[x] + a[A[bit].back()]) / 2;
A[bit].pop_back();
} else {
A[bit].emplace_back(x);
break;
}
}
int now = 0;
for (; now + 1 < (int)A[bit].size() && s[A[bit][now + 1]] == 'L'; now += 2) {
ans[A[bit][now]] = ans[A[bit][now + 1]] = (a[A[bit][now]] + a[A[bit][now + 1]]) / 2;
}
if (A[bit].size() & 1) ans[A[bit].back()] = -1;
else if (!A[bit].empty() && s[A[bit].back()] == 'R') {
ans[A[bit][now]] = ans[A[bit][now + 1]] = m + (a[A[bit][now]] - a[A[bit][now + 1]]) / 2;
}
}
for (auto x : ans) std::cout << x << ' ';
std::cout << '\n';
}
return 0;
}

D:以为最小费用最大流能过,结果 TLE 26

首先注意到如果有 $k$ 个需要移动到另外 $k$ 个地方,那么排序之后依次顺序丢进去即可。

然后设 $dp[i][j]$ 表示考虑前 $i$ 选择 $j$ 个位置作为终点(即前 $j$ 个 1 要移动到前 $i$ 个位置的 0 的地方)的最小值。状态转移就很明显了,主要是想到用这种方式 DP 很难得,哎还是我太菜了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<int> a(n + 1), pos;
for (int i = 1; i <= n; ++i) {
std::cin >> a[i];
if (a[i]) pos.emplace_back(i);
}
int k = pos.size();
const int inf = 1e9 + 2;
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(k + 1, inf));
for (int i = 0; i <= n; ++i) dp[i][0] = 0;
for (int i = 1; i <= n; ++i) {
dp[i][0] = 0;
for (int j = 1; j <= k; ++j) {
dp[i][j] = dp[i - 1][j];
if (a[i] == 0) dp[i][j] = std::min(dp[i][j], dp[i - 1][j - 1] + abs(pos[j - 1] - i));
}
}
std::cout << dp[n][k] << '\n';
return 0;
}

E,F 没时间看(D 搞得心浮气躁)

AtCoder abc201E

题意:树上任意两点的距离定义它们最短路上的边权异或和,求任意两点距离的距离之和。

做法:首先肯定要一位位的处理,但是我做的时候太早的一位位的处理导致特别慢,然后 TLE,后来细节优化终于 2343ms 过了,然后发现别人特别快。再一分析,发现

所以可以先把 dist[1][i] 求出来,再按位处理就很快了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
const int M = 1e9 + 7;

int main() {
// freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<std::vector<std::pair<int, LL>>> e(n + 1);
for (int i = 1, u, v; i < n; ++i) {
LL w;
std::cin >> u >> v >> w;
e[u].emplace_back(v, w);
e[v].emplace_back(u, w);
}
std::vector<LL> val(n + 1);
std::function<void(int, int)> dfs = [&](int u, int fa) {
for (auto &[v, w] : e[u]) if (v != fa) {
val[v] = val[u] ^ w;
dfs(v, u);
}
};
dfs(1, 0);
const int N = 60;
std::vector<int> cnt(N);
for (auto x : val) {
for (int i = 0; x; x >>= 1, ++i) if (x & 1) ++cnt[i];
}
int ans = 0;
for (int i = 0; i < N; ++i) {
ans = (ans + (1LL << i) % M * cnt[i] % M * (n - cnt[i])) % M;
}
std::cout << ans << '\n';
return 0;
}

Harbin Engineering University Collegiate Programming Contest 2021

pdf 试题官方题解

A:用 Set 对区间进行贪心筛选,然后贪心查询即可

B:签到水题

C:贪心,从左到右,打标记,用下 Set 即可

D:双指针,树状数组或线段树区间加

这里同样可以用树状数组的思路优化成 $O(n)$

E:简单递推博弈

这个设此次取值为 x,后面先手期望为 f,然后列式子发现此次先手的期望为 $|x - f|$,然后从后往前递推即可

F:简单二分

G:很有意思的一道题

首先注意到(根出发)最短的那条路径必然全部要染色,然后我们可以设 $(d, u)$ 表示以 u 为根,每条路染色为 d 的方案数,注意到如果 u 不分叉(根节点特判一下),那么我们可以一直跑直到它分叉或者到叶子节点。如果 $d = dep[u]$,那么 u 必然要被染色,否则若分叉必然不被染色,若不分叉可能会被染色,这取决于最后能跑到哪里。

H:相当的毒瘤,要特别细心

首先把它变成 $1, -1$ 交替出现的形式,然后再看 $-1$ 的个数的奇偶性

特别注意,$-1, -1, -1, 1$ 要变成 $-1, 1, 1$ 而非 $1, -1, 1$

I:签到题

J:有意思的简单 DP 题

注意对称性

K:数论题

首先这个期望与每个排列的圈的个数有关。长度为 $n$ 的排列的圈的期望为 $a_n$,那么我们显然有

这个转移是注意到:点是退化的链,且一旦形成了圈就不再变了

然后用并查集维护一下即可。

N:签到题

The 18th Zhejiang Provincial Collegiate Programming Contest:陈靖邦的高质量一场

A:签到题

C: 通过比较距离判断

D:特别经典有意思的最短路问题

题意是连的边必须满足一个编号是另一个的前缀。这样我们用 dist[u][dep] 表示 uu >> len[u] - dep 的距离,显然 $len[u] \geq dep$ 才有意思。然后任意一点的距离就是看它们到它们的公共前缀之间的距离之和的最小值。

注意再 Dijkstra 的时候,我们找边要往下找!

F:经典数学问题!要不慌不忙推理即可

枚举 n 的值,然后整除分块加速即可

L:水题

我一开始以为题目给的是 S,导致无从下手

M:推理发现,答案始终为 0

UFPE Starters Final Try-Outs 2021

B:直接暴力

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0, sz; i < n; ++i) {
std::cin >> sz;
for (int j = 0, x; j < sz; ++j) {
std::cin >> x;
a[i] |= 1 << --x;
}
}
auto check = [&](int x) {
for (int i = 0; i < n; ++i) {
if (x & (1 << i)) {
if ((x & a[i]) == a[i]) return 0;
} else {
if ((x & a[i]) != a[i]) return 0;
}
}
return 1;
};
int ans = 0;
for (int i = 0; i < (1 << n); ++i) ans += check(i);
std::cout << ans << '\n';
return 0;
}

我这写法优雅的不行,可以让题目中 N 放宽到 25,且不用对 S 有任何限制。(写的时候没输入 x,然后 WA 几次才发现,也是服了自己了)

D 简单二分

H. 8 Game 超级经典的游戏!假搜索,真 Dijkstra

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
// freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);

std::vector<std::pair<int, int>> dir = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
auto check = [&](int x, int y) {
return x >= 0 && x < 3 && y >= 0 && y < 3;
};
std::map<std::string, int> mp;
std::vector<std::string> pm;
std::string od("012345678");
do {
mp[od] = pm.size();
pm.emplace_back(od);
} while (std::next_permutation(od.begin(), od.end()));
std::string s(9, '0');
for (int i = 0; i < 9; ++i) std::cin >> s[i];
std::vector<int> c(9);
for (int i = 1; i < 9; ++i) std::cin >> c[i];
std::vector<LL> dist(pm.size(), INT64_MAX);
auto Dijkstra = [&](int s) {
dist[s] = 0;
std::priority_queue<std::pair<LL, int>> Q;
Q.push({0, s});
while (!Q.empty()) {
auto [du, u] = Q.top();
Q.pop();
if (dist[u] != -du) continue;
std::string now = pm[u];
int p0 = std::find(now.begin(), now.end(), '0') - now.begin();
std::pair<int, int> zero{p0 / 3, p0 % 3};
for (auto [dx, dy] : dir) if (check(zero.first + dx, zero.second + dy)) {
int q0 = (zero.first + dx) * 3 + zero.second + dy;
std::string cur = now;
std::swap(cur[p0], cur[q0]);
int v = mp[cur], cost = c[cur[p0] - '0'];
if (dist[v] > dist[u] + cost) {
dist[v] = dist[u] + cost;
Q.push({-dist[v], v});
}
}
}
};
Dijkstra(mp["123456780"]);
std::cout << dist[mp[s]] << '\n';
return 0;
}

注意要保存成字符串节省空间,还有就是用排列缩小编号

I:裸动态开点线段树,一开始以为 multiset 能过(pb_ds 无敌)

应该 long long 的地方 写成了 int 导致 WA 两发 很不应该

multiset 不能过是因为 distance 求的时候是 $O(N)$ 的,然而 pb_ds 就提供 $O(\log n)$ 的操作。但是 pb_ds 默认不支持 multiset 的形式,于是用 std::pair<int, int>, 后面带指标就可以变得像 multiset 了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using namespace __gnu_pbds;

#define ordered_set tree<std::pair<LL, int>, null_type, std::less<>, rb_tree_tag, tree_order_statistics_node_update>

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, q;
std::cin >> q >> n;
std::vector<LL> a(n);
ordered_set A;
for (int i = 0; i < n; ++i) A.insert({a[i], i + 1});
int cnt = n;
while (q--) {
int op;
LL k;
std::cin >> op;
if (op == 1) {
int id;
std::cin >> id >> k;
A.erase(A.lower_bound({a[id], 0}));
a[id] += k;
A.insert({a[id], ++cnt});
} else {
std::cin >> k;
std::cout << n - A.order_of_key({k + 1, 0}) << '\n';
}
}
return 0;
}

竟然比我用动态开点线段树快,我不能接受

Codeforces Round #720 (Div. 2)

加分了但是不太光彩的一场!本质上我只做了 2 题(5 题只做两题,哎)C 题最终评测的时候发现有 询问次数可能为 $2n$,然后最后我攻击了自己的代码。

A

特判一下 $b == 1$ 然后输出 $a, a b, a(b + 1)$ 即可

B

找到最小值,然后往两边递增即可

C

首先注意到 $\max(1, x) = x, \min(n, x) = x$,然后先通过比较 $1, 2$,$1, 3$ 就可以确定某一个值,然后根据这个值与 $\frac{n}{2}$ 的大小关系来确定是询问大的还是询问小的。总次数不超过为 $\frac{3n}{2} + 4$

D

一开始想错了,以为只要把度数大于 2 的剪掉放在最后面即可。后来发现剪去两边度都大于 2 的才是最优的选择。然后根据题解做了,注意要先处理儿子,最后再处理自己

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
// freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<std::set<int>> g(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
g[u].insert(v);
g[v].insert(u);
}
std::vector<int> son(n + 1);
for (int i = 1; i <= n; ++i) son[i] = g[i].size() - 1;
++son[1];
std::vector<std::set<std::pair<int, int>>> e(n + 1);
for (int i = 1; i <= n; ++i) {
for (auto x : g[i]) e[i].insert({-son[x], x});
}
std::vector<std::pair<int, int>> cut, add;
std::function<void(int, int)> dfs = [&](int u, int fa) {
for (auto [_, v] : e[u]) if (v != fa) dfs(v, u);
if (son[u] >= 2) {
if (g[u].count(fa)) {
g[u].erase(fa);
g[fa].erase(u);
cut.emplace_back(u, fa);
--son[fa];
}
}
for (auto [_, v] : e[u]) if (v != fa && son[u] > 2) {
if (g[u].count(v)) {
g[u].erase(v);
g[v].erase(u);
cut.emplace_back(u, v);
--son[u];
}
}
};
dfs(1, 0);
std::vector<int> vis(n + 1);
std::vector<std::pair<int, int>> p;
std::vector<int> tmp;
std::function<void(int, int)> find = [&](int u, int fa) {
vis[u] = 1;
if (g[u].size() <= 1) tmp.emplace_back(u);
for (auto v : g[u]) if (v != fa) {
find(v, u);
}
};
for (int i = 1; i <= n; ++i) if (!vis[i]) {
tmp.clear();
find(i, 0);
p.push_back({tmp[0], tmp.back()});
}
for (int i = 1; i < p.size(); ++i) {
add.emplace_back(p[i - 1].second, p[i].first);
}
int sz = cut.size();
std::cout << sz << '\n';
for (int i = 0; i < sz; ++i) {
std::cout << cut[i].first << ' ' << cut[i].second << ' ' << add[i].first << ' ' << add[i].second << '\n';
}
}
return 0;
}

E

找规律,规律找到了后,代码就好些了,直接看题解的图一目了然

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int cas = 1;
std::cin >> cas;
auto f = [](int n) {
return (n + 1) / 2 * n;
};
auto g = [](int n) {
return n * n - (n / 2) * (n / 2);
};
while (cas--) {
int m, k;
std::cin >> m >> k;
std::vector<std::pair<int, int>> a(k);
for (int i = 0, x; i < k; ++i) {
std::cin >> x;
a[i] = {x, i + 1};
}
std::sort(a.begin(), a.end(), std::greater<>());
int l = 0, r = 1e4, mx = a[0].first;
while (l <= r) {
int mid = (l + r) / 2;
if (f(mid) >= mx && g(mid) >= m) r = mid - 1;
else l = mid + 1;
}
int n = l;
std::vector<std::vector<int>> b(n, std::vector<int>(n));
int now = 0;
for (int i = 1; i < n; i += 2) {
for (int j = 0; j < n; j += 2) {
while (now < a.size() && a[now].first == 0) ++now;
if (now == a.size()) break;
b[i][j] = a[now].second;
--a[now].first;
}
if (now == a.size()) break;
}
for (int i = 0; i < n; i += 2) {
for (int j = 0; j < n; j += 2) {
while (now < a.size() && a[now].first == 0) ++now;
if (now == a.size()) break;
b[i][j] = a[now].second;
--a[now].first;
}
if (now == a.size()) break;
}
for (int i = 0; i < n; i += 2) {
for (int j = 1; j < n; j += 2) {
while (now < a.size() && a[now].first == 0) ++now;
if (now == a.size()) break;
b[i][j] = a[now].second;
--a[now].first;
}
if (now == a.size()) break;
}
std::cout << n << '\n';
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
std::cout << b[i][j] << " \n"[j == n - 1];
}
}
}
return 0;
}

Codeforces Global Round 14

E: 起初想的 dp 是 $n^4$ 方的,后面想到了 $O(n^3)$ 的做法以为也是 $O(n^4)$ 的就没写

dp[i][j] 表示长度为 i,有 j 个位置没有主动开启的方案数,首先 $dp[i][0] = 2^{i - 1}$ 这是因为我们考虑第一个位置,后面的只能在两边选,然后就是一个二进制的和正好是 $2^{i - 1}$,然后我们可以枚举第一个没有主动开启的位置。所以

有 $O(n^2)$ 的做法

UTPC Spring 2021 Open Contest

I

找一个长为 $n$ 的排列 $x_1, \cdots, x_n$ 使得 $\displaystyle \prod_{j = 1}^{i} \mod n$ 正好是 $0, \cdots, n - 1$ 的一个排列。

  • 做法 1:注意到除了 4 一个合数外,其它合数都做不到这一点,因为最终乘积必然是 0,非 4 的合数中间必然会出现 0。所以剩下的只需看 $n$ 为素数的情况,注意到 $\frac{i}{i - 1} = \frac{j}{j - 1}$ 当且仅当 $i = j$, 因此这就是我们的方案了。
  • 做法 2:当然了,还有原根的做法,即把乘变成加。此时我们考虑 $g^0, g^{1}, g^{n - 1 - 2}, g^{3}, \cdots$ 也是一个可行方案,得到的前缀和恰好是 $0, 1, -1, 2, -2, \cdots$

Contest 2050 and Codeforces Round #718 (Div. 1 + Div. 2): 质量高代码实现能力要求极高

B

$i, j$ 写反,debug 了 15 分钟,真的烦!导致后面用时都加了

C

超级有趣感觉,找找规律就会发现往下一跳对角线,规模就变小了,然后跑 n 次好了

D

标准 DP 没啥好说的

E

推理一下 10 分钟,就知道怎么做,但是这个代码实现是真的把自己给整吐了,这个时候就要参考一下 dreamoon_love_AA 的做法了。
用全局变量加数组,然后 i 从 1 开始,这样会让很多东西简单不少(但是我还是不想写)。

Codeforces Round #717 (Div. 2)

mohammedehab2002 的连续第二场,相对第一次质量更高,不只是单纯的数论

B: 没注意最多两个,贡献了一次 WA

如果最后只有两个,那么必然要求所有数异或和 $r = 0$ 0,如果是 3 个,每一个异或和部分都是 r。然后这正好是奇偶的情况。所以 $r = 0$ 直接就可以,否则 $r \neq 0$,那么我们碰到异或前缀和为 $r$ 就删掉并计数一次,然后看最终结果是不是 r,且计数次数大于 1

很有意思的问题

C:背包 + 奇偶性

D: 经典问题:区间划分成子区间,多次查询

合理区间等价于同一区间不能有公共的素因子。所以如果不是多次查询,我们直接贪心就能做了。多次查询我搞不了,就以为要莫队,结果学了莫队也不能转移。后来才知道是倍增算法!!

做法:预处理 N 以内所有素数,然后处理出前 N 个数的所有素因子(不算重),再 dp[i] 表示能到达右边的最远位置,即 $[i, dp[i])$ 是以 i 开头最大的合理区间。那么 dp 的时候从右往左转移即可。dp[i] 定义好了,我们就朴素倍增即可。倍增的最大高度显然不会超过 log n,因此算法复杂度为 $O(N \log N)$。

E: 经典问题:经过 k 次互换操作,最终得到的不同排列的数量

首先把它简化成,有多少个长度为 n 排列正好经过 k 次互换操作能变成有序的记作 $DP[n][k]$,那么状态转移为

最终的答案是 $dp[n][j] + dp[n - 2][j] + \cdots$。复杂度 $O(n k)$,并且我们可以优化空间为 $O(1)$ 的。但是时间复杂度还是过不了,因此我们需要寻找其它做法:

注意到 $k$ 次互换操作后,最多有 $2k$ 个位置发生了改变。

我们用生成函数 $\displaystyle f_n = \sum_{k \geq 0} dp[n][k] x^k$,注意 $f_1 = 1, f_2 = 1 + x$

然后就转换成计算这个函数的次数不高于 k 的系数了。分治做法:
$L = \sum_{i = l}^{m - 1}(1 + ix), R = \sum_{i = m}^{r - 1} (1 + ix)$ 所以可以借助于 $L$ 来计算 $R$。完全可以类似于 powMod 的计算。

hugin代码实现

另外的做法见我的 codeforces 上的评论 以及我的代码实现

Codeforces Round #716 (Div. 2)

mohammedehab2002 的连续第一场

C: 拓展 Wilson 定理

即使不用这个定理也行。原理: 首先不能放与 $n$ 不互素的元素,否则乘积必然不与 $n$ 互素,从而不可能为 $1$,把剩下的数乘起来,如果为 $1$ 就好了,不为 $1$,必然是其中一个,把那一个踢了就行。

拓展 Wilson 定理:

即 n 为原根时为 -1, 其它情况为 1.

我们判断的使用用 $= n - 1$ 来判断结果 $n = 2$ 的时候给了我一次 WA

D:概率做法真的骚气

区间有超过一半的元素就才会计数,因此,我们可以概率枚举 50 次。当然了常规做法是主席树(不会)会计数的众数必然是中位数。也可以用莫队。

Atcoder arc117C

很有趣的一个堆积问题,关键在于转化成简单的四则运算,而当时没有想到这样的运算,可惜

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

// 0 <= x < p < INT_MAX
LL powMod(LL x, LL n, LL p){
LL r = 1;
while (n) {
if (n&1) r = r * x % p;
n >>= 1; x = x * x % p;
}
return r;
}

// 一般情况下 N < 1e6, M 必须是一个小于 INT_MAX 的素数
namespace Binom {
int N = 0;
LL M = 1e9 + 7;
std::vector<LL> fac, ifac;
void setMod(LL m) {
M = m;
fac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = fac[i - 1] * i % M;
ifac[N - 1] = powMod(fac[N - 1], M - 2, M);
for (int i = N - 1; i; --i) ifac[i - 1] = ifac[i] * i % M;
}
void init(int n, LL m = M) {
N = n;
fac.resize(N);
ifac.resize(N);
setMod(m);
}
// 需要预处理小于 n 的所有值!
LL binom(int n, int k) {
if (n < 0 || n < k) return 0;
return fac[n] * ifac[k] % M * ifac[n - k] % M;
}
// 一般说来需要预处理出小于 M 的所有值,且 M 是素数!
LL lucas(LL n, LL k) {
LL r = 1;
while (n && k) {
int np = n % M, kp = k % M;
if (np < kp) return 0;
r = r * binom(np, kp) % M;
n /= M; k /= M;
}
return r;
}
} // namespace Binom

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::map<char, int> mp{{'R', 0}, {'W', 1}, {'B', 2}};
Binom::init(3, 3);
int ans = 0;
std::string s;
std::cin >> s;
for (int i = 0; i < n; ++i) {
ans += mp[s[i]] * Binom::lucas(n - 1, i);
}
ans %= 3;
if (n % 2 == 0) ans = -ans;
std::cout << "RWB"[ans < 0 ? ans + 3 : ans] << '\n';
return 0;
}

Codeforces Round #715 (Div. 2):Div2 只做三题,我挺难受的

C

意识到此问题跟原始位置无关就不难想到先排序,然后区间 DP

D

被卡死了。首先注意到如果两个序列中 1 的个数都不小于 n,那么就存在序列的分别的子序列包含它们。然后就好了。

牛客 11189C:贪心 + 并查集

我一开始搞成树上问题 TLE。有点可惜。并查集常数确实小很多。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, m;
std::cin >> n >> m;
std::vector<int> p(n + 1), fa(n + 1), c(n + 1);
for (int i = 1; i <= n; ++i) std::cin >> p[i], fa[i] = i;
for (int i = 0, x; i < m; ++i) {
std::cin >> x;
++c[x];
}
std::function<int(int)> find = [&](int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
};
auto merge = [&](int x, int y) {
fa[find(x)] = find(y);
};
for (int i = 1; i <= n; ++i) if (c[i] == 0) merge(i, p[i]);
LL ans = 0;
for (int i = n; i >= 0; --i) {
int x = find(i);
if (c[x]) {
ans += i;
if (--c[x] == 0) merge(x, p[x]);
}
}
std::cout << ans << '\n';
return 0;
}

2021 ECNU Campus Invitational Contest

质量还是很不错的。官方题解

A:简单线性代数,写的是否 f(a, b) 写成 f(a, d),然后一直出问题了

B:知道怎么处理,但是写的话很麻烦

最后终于磨出来了,从尾部开始考虑,然后判断即可。题目中 n 可以变得更大。

C: 送分题

D: 阶乘的 p 因子指数

E: 树上两点距离

F: 得到线性关系,然后一个一元二次方程即可

G: 分情况讨论一下

H: 分治,但是我不会(原始题目:COCI 2020

kczno1 的 AC 代码

本来想自动向量化暴力做这个问题,后来发现还是不行。

I:送分题,但是 我的第一次提交 有 bug 也过了(ba, aa) 应该是 NO

J: 求 $\sum_{i = 1}^n \frac{1}{i}$

求上述结果时,若 $n$ 特别大,可以求 $\sum_{i = 1}^n \frac{1}{i} \simeq \sum_{i = 1}^N \frac{1}{i} + \ln n - \ln N$

K: 仅有几个是可以的,特判一下即可

Educational Codeforces Round 107 (Rated for Div. 2)

E:用概率是最好的理解

分别考虑一整行 o 和 一整列 o 的情况。

F:如何求一个计算式(不懂)

G:暴力黑科技能搞,但是标准做法是每一位考虑

每一位考虑的好处是有很多减了之后不影响答案。

1498:很不错的一场印度赛

A: 直接暴力即可,注意到 3 的特殊性,所以最多 3 次

B: 直接贪心即可,然后用 multiset 完美契合

C:这题我卡了挺久主要是题意不明,很烦,明白题意后计算 D(k) 的个数就行

D:直接暴力 $O(n m^2)$,注意到一个点被访问后,再访问就没啥意义了。所以从后往前遍历,每个点最多访问两次,最终复杂度为 $O(n m)$

E:这题不需要交互就能做?此情况度数最小的度数前缀和为 $\frac{n(n-1)}{2}$,竟然就能缩点

F:这个经典博弈,贼 6

1484:又掉分了

C:注意到最多只有一个超过 $\lceil \frac{m}{2} \rceil$ 然后对出现频率最高的分情况讨论即可

E:我知道一致轮换模拟,注意到相邻位置 gcd 不为 1 的话,以后也永远不为 1,所以把为 1 的位置用 set 存一下然后更新即可

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<int> a(n), nxt(n);
for (auto &x : a) std::cin >> x;
for (int i = 0; i < n; ++i) nxt[i] = i + 1;
nxt.back() = 0;
std::set<int> S;
for (int i = 0; i < n; ++i) if (std::gcd(a[i], a[nxt[i]]) == 1) S.insert(i);
std::vector<int> ans;
int x = -1;
while (!S.empty()) {
auto it = S.upper_bound(x);
if (it == S.end()) it = S.upper_bound(-1);
x = *it;
ans.emplace_back(nxt[x]);
S.erase(nxt[x]);
nxt[x] = nxt[nxt[x]];
if (std::gcd(a[x], a[nxt[x]]) != 1) S.erase(x);
}
std::cout << ans.size();
for (auto x : ans) std::cout << ' ' << x + 1;
std::cout << '\n';
}
return 0;
}

E:单调栈优化 DP

f[i] 为 前 i 个数的答案。那么 $f[i] = \max_{j_0 < j < i} (b[i] + f[j], f[j_0])$ 其中 $j_0$ 为高度小于 i 的最大位置。于是我们用单调栈优化 DP 即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> h(n + 1), b(n + 1);
for (int i = 1; i <= n; ++i) std::cin >> h[i];
for (int i = 1; i <= n; ++i) std::cin >> b[i];
h[0] = INT_MIN;
std::stack<int> S;
S.push(0);
std::vector<LL> mx(n + 1, INT64_MIN), f(n + 1);
for (int i = 1; i <= n; ++i) {
mx[i] = f[i - 1];
while (h[S.top()] > h[i]) {
mx[i] = std::max(mx[i], mx[S.top()]);
S.pop();
}
f[i] = std::max(mx[i] + b[i], S.top() == 0 ? INT64_MIN : f[S.top()]);
S.push(i);
}
std::cout << f[n] << '\n';
return 0;
}

Atcode abc196F

题意:给定一个 0-1 主串 S 和模式串 T,$|T| \leq |S|$ 问 $S$ 的长度为 $|T|$ 的子串和 $T$ 互异的位置数的最小值。

显然 $f[j] = \sum_{i = 0}^{|T| - 1} s_{i+j} \wedge t_i$,这是一个减法卷积,OK 结束。

如果字符串是小写字母组成,那么这个问题就可以枚举每一个字符,看相同的位置!然后累加 $O(26 \cdot |S| \log |S|)$

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
// #include "include/izlyforever.hpp"
int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::string s, t;
std::cin >> s >> t;
std::vector<LL> a[2], b[2];
for (auto x : s) {
a[0].emplace_back(x - '0');
a[1].emplace_back('1' - x);
}
for (auto x : t) {
b[0].emplace_back(x - '0');
b[1].emplace_back('1' - x);
}
Poly A = Poly(a[0]).mulT(Poly(b[1])) + Poly(a[1]).mulT(Poly(b[0]));
auto r = A.a;
r.resize(s.size() - t.size() + 1);
LL ans = t.size();
for (auto x : r) ans = std::min(ans, x);
std::cout << ans << '\n';
return 0;
}

1499

B:删除不连续的点之后是有序的

我也没有想到我会被这题卡,真的难受。我以为只要删除了前置 0 和后置 1 之后,没有连续的 0 或 1 即可了。但是忽略了 100110 这种 case。WA 的心态爆炸,WA 的代码:

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

bool solve() {
std::string s;
std::cin >> s;
int n = s.size(), i = 0;
while (n > 0 && s[n - 1] == '1') --n;
while (i < n && s[i] == '0') ++i;
if (i == n) return 1;
int cnt[2] {};
for (int j = i + 1; j < n; ++j) if (s[j] == s[j - 1]) {
++cnt[s[j] - '0'];
}
return cnt[0] == 0 || cnt[1] == 0;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
std::cout << (solve() ? "YES\n" : "NO\n");
}
return 0;
}

下面是正确的代码:

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

bool solve() {
std::string s;
std::cin >> s;
int n = s.size(), now = 1;
for (int i = 1; i < n; ++i) if (s[i] == s[i - 1]) {
if (s[i] - '0' == now) {
if (now == 0) return false;
--now;
}
}
return true;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
std::cout << (solve() ? "YES\n" : "NO\n");
}
return 0;
}

C:经典问题

一开始就知道 奇偶要分开考虑,WA 到吐了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
auto f = [&](std::vector<LL> &a) -> std::vector<LL> {
int sz = a.size();
std::vector<LL> b(sz);
LL sm = 0, mn = INT64_MAX;
for (int i = 0; i < sz; ++i) {
sm += a[i];
mn = std::min(mn, a[i]);
b[i] = sm + mn * (n - 1 - i);
}
return b;
};
std::vector<LL> a[2];
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
a[i & 1].emplace_back(x);
}
std::vector<LL> sa[2] = {f(a[0]), f(a[1])};
LL r = INT64_MAX;
for (int i = 1; i < n; ++i) {
r = std::min(r, sa[0][i / 2] + sa[1][(i - 1) / 2]);
}
std::cout << r << '\n';
}
return 0;
}

D: gcd 类问题,还是好做的

E:动态规划

dp[i][j][0/1] 为 x 以 i 结尾, y 以 j 结尾,最终它们是否以 x 结尾的总答案数。有了这个状态,状态转移就明显了。
代码请参考 dlalswp25 的代码

1497

C2:k = 3 的情况很早就知道了,然后一般的 k 想了半天,突然发现添加 1 即可

D:这个 DP 做法我自己应该是想不到的

E:E1 贪心即可,然后 E2 又是个 DP

我们考虑 Dp[i][j] 为删除 j 个节点后,前 i 个最少可以分成多少个。那么显然 $DP[i][j] = \min DP[i_x][j - x]$,其中 $i_x, \cdots, i$,$i_x$ 删除 x 个 点后, $i_x, \cdots, i$ 所有数互异的最小的值。然后我们可以先求 $i_x$,求的时候注意细节,否则 TLE

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
// freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
const int N = 1e7 + 2;
std::vector<int> f(N), mp(N);
for (int i = 1; i * i < N; ++i) {
int ii = i * i, cur = 0;
for (int j = ii; j < N; j += ii) f[j] = ++cur;
}
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, k;
std::cin >> n >> k;
std::vector<int> a(n);
for (auto &x : a) {
std::cin >> x;
x = f[x];
}
std::vector<std::vector<int>> left(n, std::vector<int>(k + 1));
for (int j = 0; j <= k; ++j) {
int cnt = 0, now = 0;
for (int i = 0; i < n; ++i) {
if (++mp[a[i]] > 1) {
if (++cnt > j) {
while (--mp[a[now]] == 0) ++now;
++now;
--cnt;
}
}
left[i][j] = now;
}
for (int i = now; i < n; ++i) --mp[a[i]];
}
std::vector<std::vector<int>> dp(n, std::vector<int>(k + 1, n));
for (int j = 0; j <= k; ++j) dp[0][j] = 1;
for (int i = 1; i < n; ++i) {
for (int j = 0; j <= k; ++j) {
for (int x = 0; x <= j; ++x) {
dp[i][j] = std::min(dp[i][j], left[i][x] > 0 ? dp[left[i][x] - 1][j - x] + 1 : 1);
}
}
}
std::cout << dp[n - 1][k] << '\n';
}
return 0;
}

gym 102586F:经典一维多点距离总和最短问题

首先答案必然是 $\sum_{i = 1}^n |a_i - b_i|$。然后就是看分配策略了,注意到如果 $b_i < a_i$,且左边都分配好了,那么必然就可以直接分配了,否则就会出现 $a_i \leq b_i \leq b_{i + 1} \leq a_{i + 1}$ 的局面,此时对比一下答案即可。

1500:第一题都不会做人直接傻了

A:首先注意到如果有两组两个相同的,那么这四个数就是答案,否则直接暴力,由抽屉原理,很快就能解决。

B:本质就是一个拓展 gcd + 中国剩余定理 + 二分,然后注意简化一下每次二分的部分从而优化代码。

102992:南京 2020 区域赛

F:概率 + 三分

不难推出答案为 $f(x) = \frac{n x + m}{1 - q^x}$ 的整数点最小值。单峰函数所以可以三分。

J:表面 Nim sg 问题,实则吉老师线段树

用一个线段树的最小值和次小值和最小值的个数就可以维护更新最大值。

K: 简单找规律签到水题

L:排序水题

M:树上背包(特别有意思)

1495

只做了两题,不看题解我应该做不了 3 题,如果去攻击代码能加分的

B:很快就知道只有唯一的最大值点可能成为胜决策点,然后如果两边不平均,必然不是,而且最大递增长度要为奇数(忽略了导致两次 WA)

C:三等分分别处理(三分?) 确实是个好问题。可惜没有往这个方向想

1493:记错时间没参加,否则真掉分了

D:gcd 可看作素因子分解的每个因子指数的最小值,实现时要注意效率选择合适的数据结构

E: XOR 神奇的找规律(我应该是看不出来的)

注意到最高位的答案和 r 的奇偶性有很大的关系!

gym 102978XXI Opencup GP of Tokyo 官方题解

一场不看题解几乎一题不会的场

A:经典转化

即使看了题解,说等价于删除左或者删除右,我一开始还是没懂,后来注意到每次有 2N - 2 种选择之后就懂了。后来就是删除然后递推了(个人认为是平凡的)。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

// 0 <= x < p < INT_MAX
LL powMod(LL x, LL n, LL p){
LL r = 1;
while (n) {
if (n&1) r = r * x % p;
n >>= 1; x = x * x % p;
}
return r;
}

// 一般情况下 N < 1e6, M 必须是一个小于 INT_MAX 的素数
namespace Binom {
int N = 0;
LL M = 1e9 + 7;
std::vector<LL> fac, ifac;
void setMod(LL m) {
M = m;
fac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = fac[i - 1] * i % M;
ifac[N - 1] = powMod(fac[N - 1], M - 2, M);
for (int i = N - 1; i; --i) ifac[i - 1] = ifac[i] * i % M;
}
void init(int n, LL m) {
N = n;
fac.resize(N);
ifac.resize(N);
setMod(m);
}
// 需要预处理小于 n 的所有值!
LL binom(int n, int k) {
if (n < 0 || n < k) return 0;
return fac[n] * ifac[k] % M * ifac[n - k] % M;
}
// 一般说来需要预处理出小于 M 的所有值,且 M 是素数!
LL lucas(LL n, LL k) {
LL r = 1;
while (n && k) {
int np = n % M, kp = k % M;
if (np < kp) return 0;
r = r * binom(np, kp) % M;
n /= M; k /= M;
}
return r;
}
} // namespace Binom
int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
const LL M = 998244353;
Binom::init(n, M);
std::vector<LL> x(n);
x[0] = 1;
for (int i = 1; i < n; ++i) x[i] = x[i - 1] * (2 * i - 1) % M;
LL r = 0;
for (int i = 0; i < n; ++i) if (a[i] == 1) {
r += Binom::binom(n - 1, i) * x[i] % M * x[n - 1 - i] % M;
}
std::cout << r % M << "\n";
return 0;
}

C:经典 FFT(神仙题)

这不看题解咋做呀,看了题解也要搞半天。题解是这样审的:

首先考虑这个问题:给定非负整数 $W, A, B$ 问从格点 $(0, 0)$ 到格点 $(W, AW + B)$ 有多少条路径(只往右上放走)并且不许在直线 $y = Ax + B$ (严格的)上方。

可以证明答案是 $\binom{W + AW + B}{W} - A \binom{W + AW + B}{W - 1}$。这个证明完全类似 Catalan 数的计算。

接下来,我们给定非负整数 $H, W, A, B$ 满足 $0 \leq B \leq AW + B \leq H$,考虑从 $(0, 0)$ 到 $(W, H)$ 的格点路径。我们定义路径的权重为经过形如 $(x, Ax+B)$ 的点的个数。我们把所有路径的权重和记作 $f(H, W, A, B)$。考虑 $z = f(H, W, A, B) - A \cdot f(H + 1, W - 1, A, B)$,显然

考虑从 $(0, 0)$ 到 $(W, H + 1)$ 的路径个数。因为 $H + 1 > AW + B$,因此总存在最小的 $x$ 使得路径经过 $(x, Ax + B)$ 并且此后都严格在直线 $y = Ax + B$ 之上。把这后一段往下带一手,那么就是不严格在直线之上了(我们强行演算也可以)。即 $z = \binom{W + H + 1}{W}$,因此

注意到上述结果于 B 无关,因此我们不妨取 $B = 0$ 这很重要!。

我们回到原问题:我们需要计算 $\sum_{1 \leq x \leq N} g(x)$ 其中 $N = \lfloor \frac{R}{B}\rfloor$,$g(x) = (R + 1 - Bx) f(R, B, x)$
注意到 $(x, y)$ 会被直线 $Ax + B$ 经过 $\frac{y}{x}$ 次!因此就是上述计算!

卡在时间上过了,差点超时。

D:经典 FFT(神仙题)

官方题解说是使用 Tellegen’s Principle,但是 jtnydv25 给了更简洁的做法。SevenDawns 的 Talk。
利用 多点求值新科技 类似的推出,我们需要我们需要求解

其中 $T(x) = \sum_{i = 1}^n \frac{C_i}{1 - A_i x}$。我们记 $P_{[L, R]} = \prod_{i = L}^{R} (x - B_i)$。我们分治就可以得到答案

代码太长不贴了。

G: 可选多堆的二人公平博弈

问题转化成求多元多项式的 k 次方的问题。这里多元多项式是比较特殊的,而且它的乘法也比较特殊。即

它是 fwt 变换可解决的一种。当然了这也可以如果 $N = 1$ 这就是实打实的卷积,并且这个长度不可拓展!那么我们可以每一维做一次 DFT。

I

此题真的把我折磨吐了(最后开始 Coach 模式终于解决了)。注意到最后一个元素的特殊性!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const LL M = 998244353;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> vis(n);
std::vector<int> a(m);
for (auto &x : a) {
std::cin >> x;
vis[--x] = 1;
}
int k = 1;
while (k < m && a[k] > a[k - 1]) ++k;
LL r = 1;
for (int i = 0, cnt = 0, j = 0; i < n; ++i) {
if (j < k && i == a[j]) {
if (++j == k) ++j;
}
if (vis[i]) continue;
r = r * (j + cnt) % M;
++cnt;
}
std::cout << r << "\n";
return 0;
}

1494E:假图论,真找规律

首先注意到如果 u 到 v 和 v 到 u 有相同的 label,那么必然 YES,否则如果 u 到 v 和 v 到 u 都有边,那么偶数情况必然 YES,奇数情况 NO。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::map<std::pair<int, int>, char> mp;
int cnt[2]{};
for (int i = 0; i < m; ++i) {
char op;
std::cin >> op;
if (op == '?') {
int k;
std::cin >> k;
std::cout << ((cnt[0] || (cnt[1] && k % 2 == 1)) ? "YES\n" : "NO\n");
} else {
int u, v;
std::cin >> u >> v;
if (op == '+') {
char c;
std::cin >> c;
mp[{u, v}] = c;
if (mp.count({v, u})) {
++cnt[1];
if (mp[{v, u}] == c) ++cnt[0];
}
} else {
if (mp.count({v, u})) {
--cnt[1];
if (mp[{v, u}] == mp[{u, v}]) --cnt[0];
}
mp.erase({u, v});
}
}
}
return 0;
}

1494F:拓展 Euler 路

先走 Euler 路,然后走两点折返的路径(先删去两点折返路,细节就是删完之后要保证连通)

我最后攻击了自己的代码

以上代码还有很多可优化的地方,不过暂时没时间也没必要优化。最终无比丑陋的代码

1491E:经典树上问题,Fib 树

大致题意:一颗树 Fib 树当且仅当它的节点个数为 Fib 数且可以拆成两颗 Fib 树。

做法:首先所有节点个数必须是 Fib 数 $F_n$,且可以拆成有两颗 $F_{n - 1}$ 和 $F_{n- 2}$ 的子树。我们递归的看这两颗子树是不是 Fib 树即可。若有多种分拆方式,可以证明它们不会影响结果。(可画图看看就能理解,详细说明还挺麻烦)

比赛的时候知道这么做,但是怕递归超时就不敢这么写,哎!还想着怎么搞出更简洁的公式,实际上复杂度是 $O(n F_n)$ 的。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::set<int>> e(n);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
--u;
--v;
e[u].insert(v);
e[v].insert(u);
}
std::vector<int> fib{1, 1};
while (fib.back() < n) fib.emplace_back(fib[fib.size() - 2] + fib[fib.size() - 1]);
if (fib.back() != n) {
std::cout << "NO\n";
return 0;
}
std::vector<int> sz(n), fa(n);
bool flag = true;
std::function<void(int, int)> solve = [&](int u, int i) {
if (i <= 3 || !flag) return;
int r = -1;
std::function<void(int)> dfs = [&](int u) {
sz[u] = 1;
for (auto v : e[u]) if (v != fa[u]) {
fa[v] = u;
dfs(v);
sz[u] += sz[v];
}
if (sz[u] == fib[i - 1] || sz[u] == fib[i - 2]) {
r = u;
}
};
fa[u] = -1;
dfs(u);
if (r == -1) {
flag = false;
return;
}
e[r].erase(fa[r]);
e[fa[r]].erase(r);
if (sz[r] == fib[i - 1]) {
solve(fa[r], i - 2);
solve(r, i - 1);
} else {
solve(fa[r], i - 1);
solve(r, i - 2);
}
};
solve(0, fib.size() - 1);
std::cout << (flag ? "YES\n" : "NO\n");
return 0;
}

上述代码可以通过打标记来删边,不一定要用 set 或 unordered_set 存边。

1491D:经典位运算问题

大致题意:u 到 u + v 有一条有向边,当且仅当 $u \And v = v$。问 $x$ 能否到达 $y$。

做法:首先注意到若 $x > y$ 肯定不行,从 $x$ 出发的路径中二进制中 1 的个数只减不增。并且任何后缀的二进制中 1 的个数也是只减不增的。可以归纳证明此为 x 可达 y 的充要条件。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

bool solve() {
int u, v;
std::cin >> u >> v;
if (u > v) return 0;
int x = 0, y = 0;
for (int i = 0; i < 30; ++i) {
x += u % 2;
y += v % 2;
u /= 2;
v /= 2;
if (x < y) return 0;
}
return 1;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
std::cout << (solve() ? "YES\n" : "NO\n");
}
return 0;
}

1491C:经典延迟更新问题

大致题意:给定初始数组 S, 每次经过 i 后,下一步就会到达 $i + S_i$ 直到值大于 n,然后 $S_i$ 减 1,且不会小于 1。问至少多少次可以使得所有的 S 都变成 1。

做法:显然从左往右贪心即可。但是要延迟更新,不然 TLE,延迟更新写的时候,记得只记录往后走的一步。具体来讲用 b[i] 表示 i 位置已被前面的位置走了 b[i] 次。因此 ans += std::max(0, a[i] - b[i] - 1),然后 $i + 2, \cdots, i + a[i]$ 的 b[i] 都要加 1。直接这么做复杂度 $O(n^2)$,我们可以用线段树,或者 set,或者拓展树状数组优化到 $O(n \log n)$(不写了)。然而次问题可以优化到 $O(n)$(不可思议)做法本质和拓展树状数组思路一致。记 $c[i] = b[i] - b[i - 1]$ 即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<LL> a(n), b(n + 1);
for (auto &x : a) std::cin >> x;
LL r = 0;
for (int i = 0; i < n; ++i) {
for (int j = 2; j <= a[i] && i + j < n; ++j) ++b[i + j];
if (b[i] >= a[i]) {
b[i + 1] += b[i] - a[i] + 1;
} else {
r += a[i] - b[i] - 1;
}
}
std::cout << r << "\n";
}
return 0;
}

$O(n)$ 做法

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<LL> a(n + 1), c(n + 1);
for (int i = 1; i <= n; ++i) std::cin >> a[i];
LL r = 0, b = 0;
for (int i = 1; i <= n; ++i) {
b += c[i];
if (b >= a[i]) {
if (i + 1 <= n) {
c[i + 1] += b - a[i] + 1;
if (i + 2 <= n) c[i + 2] -= b - a[i] + 1;
}
} else {
r += a[i] - b - 1;
}
if (i + 2 <= n) ++c[i + 2];
if (i + a[i] < n) --c[i + a[i] + 1];
}
std::cout << r << "\n";
}
return 0;
}

1492D:经典二进制

二进制为 a 位 0 和 b 位 1 的两个数 x, y($x \geq y$,且无前导 0)。问是否存在 x, y 使得 $x - y$ 的二进制有且仅有 k 个 1。一开始想错了!后来发现,如果只有两个 1,仅需 $2^{k + 1} - 1$ 即可。然后多余的 1 可以 x 和 y 每个位置都有即可,但不要回到了 k + 1 位 后最后一位。然后注意边界判断!(一开始忘了特判 a = 0,然后 hack 的时候才发现,提交前复制变成剪切导致 WA 一次,吐了,不过最后此场还是加分了。)

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int a, b, k;
std::cin >> a >> b >> k;
if (b == 1) {
if (k == 0) {
std::cout << "Yes\n";
std::cout << "1" << std::string(a, '0') << "\n";
std::cout << "1" << std::string(a, '0') << "\n";
} else {
std::cout << "No\n";
}
} else {
if (a == 0) {
if (k == 0) {
std::cout << "Yes\n";
std::cout << std::string(b, '1') << "\n";
std::cout << std::string(b, '1') << "\n";
} else {
std::cout << "No\n";
}
} else if (k <= a + b - 2) {
std::cout << "Yes\n";
std::string x(a + b, '0'), y(a + b, '0');
x[0] = y[0] = '1';
x[a + b - k - 1] = '1';
y.back() = '1';
int t = b - 2;
for (int i = 1; t && i < a + b; ++i) if (i != a + b - k - 1) {
x[i] = y[i] = '1';
--t;
}
std::cout << x << "\n";
std::cout << y << "\n";
} else {
std::cout << "No\n";
}
}
return 0;
}

1492E:经典纠错码

给定 m 个 n 维 向量,求一个 n 维向量使得它和所有其它向量不同的值的个数不超过 k = 2(若没有输出 No)。

首先看 k = 2 的情况,我们可以以第一个向量为基准,看它和其它向量值不同的个数。若超过 4,那么显然不能,若全不超过 2,那么显然可以。所以需要考虑不同值为 3 或 4 的情况。无论何种情况,相同的部分必然是最终答案的部分(否则一但不同就有两个位置都不同,必然不行),因此仅仅需要枚举即可。那么一般的 k 呢?怎么写实现呢(得到纠错码模板)。

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
96
97
98
99
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
// #include "include/izlyforever.hpp"

// a 单调递增,且最大值小于 mx。a 变成下一个字典序大于自身的序列
bool next(std::vector<int> &a, int mx) {
int n = a.size(), i = 1;
while (i <= n && a[n - i] == mx - i) ++i;
if (i > n) return false;
++a[n - i];
for (int j = 1; j < i; ++j) {
a[n - j] = a[n - i] + (i - j);
}
return true;
}

class ECC {
std::vector<std::vector<int>> a; // 原始数据 n 个 m 维向量
int k; // 容许的最大不同个数
std::vector<std::vector<int>> bad; // 与 r 不同的个数
int n, m, mxId;
void updateMxId(int i) { if (bad[i].size() > bad[mxId].size()) mxId = i;}
bool dfs(int c) { // 当前 r 剩余可改变的次数
auto bd = bad[mxId];
if (bd.size() <= k) return true;
if (bd.size() - k > c) return false;
// 注意到此时 bd 是 O(k) 的而不是 O(m) 的
std::vector<int> f(bd.size() - k);
iota(f.begin(), f.end(), 0);
int tMxId = mxId;
do {
mxId = tMxId;
std::queue<int> tmp;
for (auto x : f) {
tmp.push(r[bd[x]]);
for (int i = 0; i < n; ++i) {
if (a[i][bd[x]] == r[bd[x]]) {
bad[i].emplace_back(bd[x]);
}
if (a[i][bd[x]] == a[mxId][bd[x]]) {
bad[i].erase(std::find(bad[i].begin(), bad[i].end(), bd[x]));
}
}
r[bd[x]] = a[mxId][bd[x]];
}
for (int i = 0; i < n; ++i) updateMxId(i);
if (dfs(c - f.size())) return true;
for (auto x : f) {
for (int i = 0; i < n; ++i) {
if (a[i][bd[x]] == r[bd[x]]) {
bad[i].emplace_back(bd[x]);
}
if (a[i][bd[x]] == tmp.front()) {
bad[i].erase(std::find(bad[i].begin(), bad[i].end(), bd[x]));
}
}
r[bd[x]] = tmp.front();
tmp.pop();
}
} while (next(f, bd.size()));
return false;
}
public:
std::vector<int> r; // m 维向量,表示当前答案
ECC(std::vector<std::vector<int>> _a) : a(_a), r(a[0]) {
n = a.size(); m = r.size();
bad.resize(n);
mxId = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) if (a[i][j] != r[j]) {
bad[i].emplace_back(j);
}
updateMxId(i);
}
}
void setK(int _k) { k = _k;}
bool solve() { return dfs(k);}
};

int main() {
// freopen("C:\\Users\\dna049\\cf\\in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> a(n, std::vector<int>(m));
for (auto &x : a) for (auto &i : x) std::cin >> i;
ECC A(a);
A.setK(2);
if (A.solve()) {
std::cout << "Yes\n";
for (auto x : A.r) std::cout << x << " ";
std::cout << "\n";
} else {
std::cout << "No\n";
}
return 0;
}

1486A:低级错误还一直没发现

1
2
3
4
5
6
7
8
9
10
11
bool solve() {
int n;
std::cin >> n;
LL r = 0;
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
r += (x - i);
if (r < 0) return 0;
}
return 1;
}

这什么水平?数据还没读完你给我结束了?

1486C:交互题,每次询问区间第二大

这个题,我一直在想是不是要用 三分法。我在想啥呢。没有充分利用第二大始终是全局第二大。你知道的区间范围和你查询的区间范围可以是不同的!意识到这一点简单二分就能做了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

std::map<std::pair<int, int>, int> mp;
int cnt = 0;
int query(int l, int r) {
if (l == r) return --cnt;
if (mp.count({l, r})) return mp[{l, r}];
std::cout << "? " << l << " " << r << std::endl;
int ans;
std::cin >> ans;
return mp[{l, r}] = ans;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
int l = 1, r = n, q = query(l, r);
while (l < r) {
int m = (l + r) / 2;
if (q <= m) {
if (l == m && q == l) {
l = m + 1;
continue;
}
int qq = query(std::min(q, l), m);
if (qq == q) r = m;
else l = m + 1;
} else {
if (m + 1 == r && q == r) {
r = m;
continue;
}
int qq = query(m + 1, std::max(r, q));
if (qq == q) l = m + 1;
else r = m;
}
}
std::cout << "! " << r << std::endl;
return 0;
}

1486D:动态中位数

如果区间长度固定,我们可以用树状数组,每次添加元素删除元素即可。但是区间长度不固定,仅仅是限制了区间长度不小于 k。没有什么好的办法,但是可以将答案进行二分,然后把所有元素变成 1 或 -1,然后如果有区间长度大于 k 且和为正,那就说明答案可取。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<int> a(n + 1);
// 从 1 开始标号会方便很多
for (int i = 1; i <= n; ++i) std::cin >> a[i];
auto f = [&](int m) {
std::vector<int> b(n + 1), c(n + 1);
for (int i = 1; i <= n; ++i) b[i] = (a[i] >= m ? 1 : -1);
for (int i = 1; i <= n; ++i) b[i] += b[i - 1];
for (int i = 1; i <= n; ++i) {
c[i] = std::min(c[i - 1], b[i]);
}
for (int i = k; i <= n; ++i) if (b[i] > c[i - k]) return true;
return false;
};
int l = 1, r = n;
while (l <= r) {
int m = (l + r) / 2;
if (f(m)) l = m + 1;
else r = m - 1;
}
std::cout << r << "\n";
return 0;
}

1487B:直线变环

如果这个问题一开始就在环上考虑就能秒做了,浪费自己太多时间了。

1487C:所有选手比赛得分一致

首先注意到如果所有选手个数为 奇数,那么它们可以输赢对半;否则至少需要平一场。

1487E:经典包含冲突的选择,MEX 应用

我一开始以为是网络流,可是复杂度不支持我这么想,然后我就想着贪心然后 DP,一开始以为复杂度不行,后来想想连边的条数不超过 1e5,那必然可以呀。基本上我们对每一个位置,想上一次合法的最小。把上一次进行排序,然后 mex 一下就可以了,哎写的太慢了好气啊!(为什么不老老实实的做掉这题,做掉了就能加分了!)

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const int INF = 5e8;
std::vector<int> solve(const std::vector<int> &a, int m) {
int n = a.size();
std::vector<int> id(n), ai(n);
std::iota(id.begin(), id.end(), 0);
std::sort(id.begin(), id.end(), [&](int i, int j) {
return a[i] < a[j];
});
for (int i = 0; i < n; ++i) ai[id[i]] = i;
int p;
std::vector<int> b(m);
std::vector<std::set<int>> c(m);
std::cin >> p;
for (int i = 0, x, y; i < p; ++i) {
std::cin >> x >> y;
--x; --y;
c[y].insert(ai[x]);
}
auto mex = [&](const std::set<int> &d) {
for (int i = 0; i < n; ++i) if (!d.count(i)) return i;
return n;
};
for (int i = 0; i < m; ++i) {
int j = mex(c[i]);
b[i] = (j == n ? INF : a[id[j]]);
}
return b;
};
int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n[4];
for (int i = 0; i < 4; ++i) std::cin >> n[i];
std::vector<int> a[4];
for (int i = 0; i < 4; ++i) a[i].resize(n[i]);
for (int i = 0; i < 4; ++i) for (auto &x : a[i]) std::cin >> x;
for (int i = 1; i < 4; ++i) {
auto t = solve(a[i - 1], n[i]);
for (int j = 0; j < n[i]; ++j) a[i][j] += t[j];
}
int mx = *std::min_element(a[3].begin(), a[3].end());
std::cout << (mx < INF ? mx : -1) << "\n";
return 0;
}

官方题解 给了一个更好的做法:每一个位置先删掉冲突点,再加上这些冲突点,注意可能有重复,因此要用 multiset。

1487G:经典组合问题

给定 $c_1$ 个 a,.., $c_26$ 个 z,问你有多少个满足长度为 n 且任意子串都不是奇数长的回文的字符串。

如果没有回文的限制,那么答案自然就是 $\prod_{i = 1}^{26} \sum_{j = 0}^{c_i} \frac{x^j}{j!}$ 在 $x^n$ 下的系数。

1485F:经典 DP

dp[i][j] 表示长度为 i 和为 j 的 hybrid 的个数。状态转移 dp[i][j] -> dp[i + 1][j + b[i]] 以及 dp[i][j] -> dp[i + 1][b[i]]。注意到 如果 $j = 0$ 就重复了。

我们利用水涨船高技术。每次操作 key 值都加 b[i] 那么转移 1 相当于没搞,转移 2 即使添加 key 为 0 的值,当前所有数的和。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const LL M = 1e9 + 7;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::map<LL, LL> mp{{0, 1}};
// -key 是水位,sm 是所有元素之和。
LL key = 0, sm = 1;
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
LL a = mp[key];
mp[key] = sm;
key -= x;
sm = (sm * 2 - a + M) % M;
}
std::cout << sm << "\n";
}
return 0;
}

1485E:经典 DP

给一个每个叶子节点深度都为 d 的有根树,从根开始有红蓝两个硬币,每轮

  • 红色硬币选择到达某一个儿子节点
  • 蓝色硬币选择移动到任何一个深度比自己大 1 的节点。
  • 红蓝硬币可以互换位置

每一轮它们都能得到 $|a_r - a_b|$ 分,问最终移动到叶子节点时最高得分。

我们设 dp[i]:此轮结束红色节点在 i 节点位置,当前的最大得分。我们考虑状态转移

若此轮第二步结束时节点 i 上是红色硬币,那么 $dp[i] = dp[fa[i]] + \max_{dist[j] = dist[i]} |a_i - a_j|$(显然我们选择最小或者最大的 $a_j$)。反之我们需要最大化 $dp[fa[j]] + |a_j - a_i|$,因为我们需要最大化 $dp[fa[j]] + a_j$ 和 $dp[fa[j]] - a_j$。所以最终我们有一个 $O(n)$ 的算法。

1485D:奇偶性问题

矩阵中原始元素小于等于 16,新元素需要时原来元素的倍数,且使得相邻元素的绝对值是一个非 0 四次方数。取一下 lcm 可以使得所有元素差值都为 0,然后这个四次方数可以取原来数的四次方(分奇偶)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
const int lcm = 720720;
for (int i = 0; i < n; ++i) {
for (int j = 0, x; j < m; ++j) {
std::cin >> x;
if ((i + j) & 1) {
std::cout << lcm + x * x * x * x << "\n";
} else std::cout << lcm << "\n";
}
}
return 0;
}

1485C:整除分块

题意:问满足 $\lfloor \frac{a}{b} \rfloor = a \mod b, 1 \leq a \leq x, 1 \leq b \leq y$ 的 $(a, b)$ 有多少对。

做法:上式等价于 $a = (\lfloor \frac{a}{b} \rfloor)(b + 1)$。所以 $a = (b + 1) n$ 且 $1 \leq n < b$。我们固定 b,则答案就是

然后整除分块即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
LL x, y;
std::cin >> x >> y;
LL r = 0, b = 3;
++y;
while (b <= y && b * (b - 2) <= x) {
r += b - 2;
++b;
}
y = std::min(x, y);
for (LL j; b <= y; b = j + 1) {
j = std::min(y, x / (x / b));
r += (x / b) * (j - b + 1);
}
std::cout << r << "\n";
}
return 0;
}

1480C:交互题,找一个排列中的极小值点

首先让左边相邻两个降序,右边相邻两个升序(否则直接结束了),然后二分比较 m 与 m + 1 即可

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int solve() {
int n;
std::cin >> n;
std::vector<int> a(n + 1);
auto query = [&](int x) {
if (a[x]) return a[x];
std::cout << "? " << x << std::endl;
int r;
std::cin >> r;
return r;
};
a[1] = query(1);
if (a[1] == 1) return 1;
a[2] = query(2);
if (a[2] > a[1]) return 1;

a[n] = query(n);
if (a[n] == 1) return n;
a[n - 1] = query(n - 1);
if (a[n - 1] > a[n]) return n;

int l = 2, r = n - 1;
while (l < r) {
int m = (l + r) / 2;
a[m] = query(m);
a[m + 1] = query(m + 1);
if (a[m] > a[m + 1]) {
l = m + 1;
} else {
r = m;
}
}
return r;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int r = solve();
std::cout << "! " << r << std::endl;
return 0;
}

1481E:经典 DP

题意:每次从数列 a 中取出一个数放在尾部,使得相同的数字挨在一起,问最少需要的次数。

做法:首先反过来考虑,看最多能有多少数维持不动(一下子简单了不少,如果直接处理原问题,会要考虑移动顺序就很难考虑了)我们显然是从后往前好考虑一些,我们设 dp[i] 为 $[i, n]$ 中最多维持不动的数。我们考虑状态转移,我们可以删除第 i 个数,此时 dp[i] = dp[i + 1],否则我们保留 $[i, n]$ 中所有值为 a[i] 的数,如果更进一步 $[1, i - 1]$ 中不再出现 a[i],那么我们可以把最后一个出现 a[i] 后面的也拿到,反之不能拿到的原因是,前面的 a[i] 要删掉要放在后面,因此,最后一个 a[i] 后面什么都不能放。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n + 1), l(n + 1), r(n + 1), cnt(n + 1), dp(n + 2);
for (int i = 1; i <= n; ++i) {
std::cin >> a[i];
r[a[i]] = i;
}
for (int i = n; i > 0; --i) l[a[i]] = i;
// dp[i] 表示 [i, n] 中能保存下来最多的数
for (int i = n; i > 0; --i) {
++cnt[a[i]];
if (l[a[i]] == i) {
dp[i] = std::max(dp[i + 1], cnt[a[i]] + dp[r[a[i]] + 1]);
} else {
dp[i] = std::max(dp[i + 1], cnt[a[i]]);
}
}
std::cout << n - dp[1] << "\n";
return 0;
}

708C:经典换根 DP

题意:是否能过通过一次调整(删一条边,加一条边)使得删除该点后的最大连通分支节点个数不超过 $\frac{n}{2}$

做法:首先考虑节点 1 为根,如果所有与 1 相连的子树的节点数均不超过 $\frac{n}{2}$,那么不用操作,已经可以作为重心了,否则最多只有一个子树的节点数大于 $\frac{n}{2}$,那么我们需要在这个子树中找一个节点个数不超过 $\frac{n}{2}$ 的子树,把它删了然后剩下的节点个数还要不超过 $\frac{n}{2}$(自然删节点最多的那个)。所以需要预处理出这个值。然后需要考虑换根(状态转移)。现在假设 fa 的结果已经搞定了,我们要看它的儿子节点 u,如果 u 的所有儿子的节点数都小于 $\frac{n}{2}$,那么我们要看 sz[1] - sz[u] 是否也小于 $\frac{n}{2}$,如果是不用操作了,否则,我们就要看抛弃以 v 为节点的子树后,整棵树节点不超过 $\frac{n}{2}$ 的最大子树(为此,我们需要预处理最大子儿子和次大子儿子)。如果 u 有个儿子节点数大于 $\frac{n}{2}$,那么和 1 一样判断即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::vector<int>> e(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
// subtree[u] 表示以 u 为根的子树中 sz 不超过 n / 2 中最大的子子树的大小。
// mx[u], mmx[u] 分别表示以表示以 u 为根的子树中 sz 不超过 n / 2 的中最大和次大的真子子树的大小。
std::vector<int> sz(n + 1), mx(n + 1), mmx(n + 1), subtree(n + 1);
std::function<void(int, int)> pdfs = [&](int u, int fa) -> void {
sz[u] = 1;
for (auto v : e[u]) if (v != fa) {
pdfs(v, u);
sz[u] += sz[v];
subtree[u] = std::max(subtree[u], subtree[v]);
if (subtree[v] > mx[u]) {
mmx[u] = mx[u];
mx[u] = subtree[v];
} else if (subtree[v] > mmx[u]) {
mmx[u] = subtree[v];
}
}
if (sz[u] <= n / 2) subtree[u] = sz[u];
};
pdfs(1, 0);
std::vector<int> mxsub(n + 1), ans(n + 1);
std::function<void(int, int)> dfs = [&](int u, int fa) -> void {
ans[u] = 1;
mxsub[u] = mxsub[fa];
if (subtree[u] == mx[fa]) {
mxsub[u] = std::max(mxsub[fa], mmx[fa]);
} else {
mxsub[u] = std::max(mxsub[fa], mx[fa]);
}
if (sz[1] - sz[u] <= n / 2) mxsub[u] = std::max(mxsub[u], sz[1] - sz[u]);
if (sz[u] <= n / 2) {
if (sz[1] - sz[u] - mxsub[u] > n / 2) ans[u] = 0;
} else {
for (auto v : e[u]) if (v != fa && sz[v] > n / 2) {
if (sz[v] - subtree[v] > n / 2) ans[u] = 0;
}
}
for (auto v : e[u]) if (v != fa) dfs(v, u);
};
dfs(1, 0);
for (int i = 1; i <= n; ++i) std::cout << ans[i] << " \n"[i == n];
return 0;
}

1324F:经典换根 DP

题意:以树的某个节点为根的子树的白色节点减去黑色节点的个数的最大值

做法:首先以 1 为根预处理所有子树的答案,然后分情况状态转移。具体看子节点是否对父节点做了贡献。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; ++i) {
std::cin >> a[i];
if (a[i] == 0) a[i] = -1;
}
std::vector<std::vector<int>> e(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
std::vector<int> ans(n + 1);
std::function<void(int, int)> pdfs = [&](int u, int fa) -> void {
ans[u] = a[u];
for (auto v : e[u]) if (v != fa) {
pdfs(v, u);
if (ans[v] > 0) ans[u] += ans[v];
}
};
pdfs(1, 0);
// 预处理出 1 为根的结果,然后进行换根 DP
std::function<void(int, int)> dfs = [&](int u, int fa) -> void {
for (auto v : e[u]) if (v != fa) {
if (ans[v] < 0) {
if (ans[u] > 0) ans[v] += ans[u];
} else {
ans[v] = std::max(ans[v], ans[u]);
}
dfs(v, u);
}
};
dfs(1, 0);
for (int i = 1; i <= n; ++i) std::cout << ans[i] << " \n"[i == n];
return 0;
}

1476D:模拟题

题意:$n + 1$ 个点排成一排,中间有 n 跳有向边,问从 i 点出发最多可以经过多少个点(没走一次,所有边都会反向一次)

做法:首先,如果不反向,显然可以从左到右,从右到左跑两次得到结果。现在反向了,我们可以把奇数边反向,或者把偶数边反向,两个都需要,用数组好标号。注意逻辑要清晰了,怎么存都想清楚了,列好式子再写代码就会很优雅。

下面代码写的相当优雅! 看了 WZYYN 的代码 发现他比我这代码优雅多了。。直接 f[0], g[0] 表示往前往后没改变时的答案,f[1], g[1] 表示改变了的答案。然后递推就好了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

std::pair<std::vector<int>, std::vector<int>> f(std::string s) {
int n = s.size();
std::vector<int> a(n + 1, 0), b(n + 1, 0);
for (int i = 0; i < n; ++i) {
if (s[i] == 'L') a[i + 1] = a[i] + 1;
}
for (int i = n - 1; i >= 0; --i) {
if (s[i] == 'R') b[i] = b[i + 1] + 1;
}
return {a, b};
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::string s;
std::cin >> n >> s;
std::string ss[2];
ss[0] = ss[1] = s;
for (int i = 1; i < n; i += 2) ss[0][i] = (ss[0][i] == 'L' ? 'R' : 'L');
for (int i = 0; i < n; i += 2) ss[1][i] = (ss[1][i] == 'L' ? 'R' : 'L');
std::vector<int> a[2], b[2];
std::tie(a[0], b[0]) = f(ss[0]);
std::tie(a[1], b[1]) = f(ss[1]);
std::vector<int> c(n + 1, 1);
for (int i = 0; i < n; ++i) {
if (s[i] == 'L') {
c[i + 1] += a[i & 1][i + 1];
} else {
c[i] += b[i & 1][i];
}
}
for (int i = 0; i <= n; ++i) std::cout << c[i] << " \n"[i == n];
}
return 0;
}

gym 102823:NTT + 生成函数

首先注意到最终结果必然是形式为 $r[i] = c_0 A[i] + \cdots c_{n - 1 - i} A[n - 1]$ 的样子,并且这些 $c_i$ 都只和 m, L 有关,实际上它们的系数不难看出是 $(1 + x + \cdot x^{L- 1})^m$ 的系数,并且我们计算的时候只要模 $x^n$ 即可。注意到上式可以直接生成函数开方直接求解,复杂度为 $O(n \log n \log m)$,当然了注意到 $(1 + x + \cdot x^{L- 1})^m = (1 - x^L)^m (1 - x)^{-m}$,直接二项式展开求一次乘法,所以可以做到整体复杂度为 $O(n \log n)$。我也是这个题的最佳解答

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
96
97
98
99
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const LL M = 998244353, ROOT = 3;
LL powMod(LL x, int n) {
LL r(1);
while (n) {
if (n & 1) r = r * x % M;
n >>= 1;
x = x * x % M;
}
return r;
}
void bitreverse(std::vector<LL> &a) {
for (int i = 0, j = 0; i != a.size(); ++i) {
if (i > j) std::swap(a[i], a[j]);
for (int l = a.size() >> 1;
(j ^= l) < l; l >>= 1);
}
}

void ntt(std::vector<LL> &a, bool isInverse = false) {
LL g = powMod(ROOT, (M - 1) / a.size());
if (isInverse) {
g = powMod(g, M - 2);
LL invLen = powMod(LL(a.size()), M - 2);
for (auto & x: a) x = x * invLen % M;
}
bitreverse(a);
std::vector<LL> w(a.size(), 1);
for (int i = 1; i != w.size(); ++i) w[i] = w[i - 1] * g % M;
auto addMod = [](LL x, LL y) {
return (x += y) >= M ? x -= M : x;
};
for (int step = 2, half = 1; half != a.size(); step <<= 1, half <<= 1) {
for (int i = 0, wstep = a.size() / step; i != a.size(); i += step) {
for (int j = i; j != i + half; ++j) {
LL t = (a[j + half] * w[wstep * (j - i)]) % M;
a[j + half] = addMod(a[j], M - t);
a[j] = addMod(a[j], t);
}
}
}
}
void mul(std::vector<LL>& a, std::vector<LL> b) {
int sz = 1, tot = a.size() + b.size() - 1;
while (sz < tot) sz *= 2;
a.resize(sz);
b.resize(sz);
ntt(a);
ntt(b);
for (int i = 0; i != sz; ++i) a[i] = a[i] * b[i] % M;
ntt(a, 1);
a.resize(tot);
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
const int N = 1e5 + 2;
std::vector<LL> inv(N);
inv[1] = 1;
for (int i = 2; i < N; ++i) inv[i] = (M - M / i) * inv[M % i] % M;
int cas = 1;
std::cin >> cas;
for (int T = 1; T <= cas; ++T) {
std::cout << "Case " << T << ": ";
int n, L, m;
std::cin >> n >> L >> m;
std::vector<LL> a(n);
for (auto &x : a) std::cin >> x;
std::reverse(a.begin(), a.end());
std::vector<LL> b(n);
LL now = 1;
for (int i = 0; i < n; ++i) {
b[i] = now;
now = now * (m + i) % M * inv[i + 1] % M;
}
mul(a, b);
a.resize(n);
std::fill(b.begin(), b.end(), 0);
now = 1;
for (int i = 0; i * L < n; ++i) {
b[i * L] = (now + M) % M;
now = -now * (m - i) % M * inv[i + 1] % M;
}
mul(a, b);
a.resize(n);
// c[i] = a[i] * b[0] + \cdots a[n - 1] * b[n - 1 - i]
// 令 d[i] = a[n - 1 - i], e[i] = c[n - 1 - i] 则
// e[i] = d[i] * b[0] + ... + d[0] * b[i]
std::reverse(a.begin(), a.end());
for (int i = 0; i < n; ++i) std::cout << a[i] << " ";
std::cout << "\n";
}
return 0;
}

gym 102823:分块贪心

题意:把两个数组a, b合并成一个数组c 保持元素原有的顺序,使得 $\sum_{i = 1}^{n + m} c_i$ 最小。

首先观察到两个事实:1. 最终 c 数组中如果有相邻元素分别在不同的原数组,那么在前面的必然更大。2. 由于我们想尽量让大的元素放在前面,但是又要保持元素原有的性质,这样就会导致大的元素推自己前面的元素跑,例如 n 个 a 中元素,和 m 个 b 中元素,一个放前面一个放后面,那么哪一个放前面呢,计算之后会发现,平均值大的放前面即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

std::vector<std::pair<int, int>> f(std::vector<LL> &a, std::vector<LL> &s) {
// 从小到大一层一层的 push 数字只会越来越大! 大致懂了
int n = a.size();
std::stack<int> Q;
Q.push(0);
for (int i = 1; i < n; ++i) {
if (s[i] - s[Q.top()] <= a[i] * (i - Q.top())) {
int t = Q.top();
Q.pop();
while (!Q.empty() && (s[t] - s[Q.top()]) * (i + 1 - t) <= (s[i + 1] - s[t]) * (t - Q.top())) {
t = Q.top();
Q.pop();
}
Q.push(t);
} else Q.push(i);
}
std::vector<std::pair<int, int>> r;
int now = n;
while (!Q.empty()) {
r.emplace_back(Q.top(), now);
now = Q.top();
Q.pop();
}
return r;
}

LL solve() {
int n, m;
std::cin >> n >> m;
std::vector<LL> a(n), b(m);
for (auto &x : a) std::cin >> x;
for (auto &x : b) std::cin >> x;
std::vector<LL> sa(n + 1), sb(m + 1);
for (int i = 0; i < n; ++i) sa[i + 1] = sa[i] + a[i];
for (int i = 0; i < m; ++i) sb[i + 1] = sb[i] + b[i];
auto fa = f(a, sa), fb = f(b, sb);
std::vector<LL> c;
while (!fa.empty() && !fb.empty()) {
auto [la, ra] = fa.back();
auto [lb, rb] = fb.back();
if ((sa[ra] - sa[la]) * (rb - lb) >= (sb[rb] - sb[lb]) * (ra - la)) {
fa.pop_back();
for (int i = la; i < ra; ++i) c.emplace_back(a[i]);
} else {
fb.pop_back();
for (int i = lb; i < rb; ++i) c.emplace_back(b[i]);
}
}
if (fa.empty()) {
for (int i = fb.back().first; i < m; ++i) c.emplace_back(b[i]);
} else {
for (int i = fa.back().first; i < n; ++i) c.emplace_back(a[i]);
}
LL ans = 0;
for (int i = 0; i < n + m; ++i) ans += c[i] * (i + 1);
return ans;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
for (int T = 1; T <= cas; ++T) {
std::cout << "Case " << T << ": " << solve() << "\n";
}
return 0;
}

1478D:GCD once more

此题,一眼看出系数之和恒定为 1,但是我一直想根据 k 的奇偶性递推,但是复杂度完全无法预测,一直卡着自己被卡炸了!
通过 2x - y 这样一直搞,那么最终只要满足 系数之和为 1,都会出现。所以这就是个 gcd 问题啊

有整数解,当且仅当

有解。当且仅当 $\gcd(a_1 - a_i, \cdots, a_n - a_i) | k - a_i$

我们可以取 $a_i$ 为最小值,这样求 gcd 就不会出现负数了。代码太简单就不写了。

gym 102832H:二分图博弈

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

class Dinic {
int n;
// e[i] 表示第 i 条边的终点和容量,注意存边的时候 e[i ^ 1] 是 e[i] 的反向边。
// g[u] 存的是所有以 u 为起点的边,这就很像链式前向星的做法
std::vector<std::pair<int, int>> e;
std::vector<std::vector<int>> g;
std::vector<int> cur, h;
// h[i] 表示 bfs 从 s 到 i 的距离,如果找到了 t,那么就说明找到了增广路。
bool bfs(int s, int t) {
h.assign(n, -1);
std::queue<int> Q;
h[s] = 0;
Q.push(s);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for (auto i : g[u]) {
auto [v, c] = e[i];
if (c > 0 && h[v] == -1) {
h[v] = h[u] + 1;
Q.push(v);
}
}
}
return h[t] != -1;
}
// f 表示从 u 点出发拥有的最大流量,输出的是 u 到 t 的最大流量
int dfs(int u, int t, int f) {
if (u == t || f == 0) return f;
int r = f;
for (int &i = cur[u]; i < g[u].size(); ++i) {
int j = g[u][i];
auto [v, c] = e[j];
if (c > 0 && h[v] == h[u] + 1) {
int a = dfs(v, t, std::min(r, int(c)));
e[j].second -= a;
e[j ^ 1].second += a;
r -= a;
if (r == 0) return f;
}
}
return f - r;
}
public:
Dinic(int _n) : n(_n), g(n) {}
void addEdge(int u, int v, int c) {
if (u == v) return;
g[u].emplace_back(e.size());
e.emplace_back(v, c);
g[v].emplace_back(e.size());
e.emplace_back(u, 0);
}
int maxFlow(int s, int t) {
int r = 0;
while (bfs(s, t)) {
cur.assign(n, 0);
r += dfs(s, t, INT_MAX);
}
return r;
}
};

bool solve () {
int nn, m, ss;
std::cin >> nn >> m >> ss;
int n = 1;
for (int i = 0; i < nn; ++i) n *= 10;
Dinic g(n + 2);
int s = n, t = n + 1;
std::vector<int> ban(n);
for (int i = 0, x; i < m; ++i) {
std::cin >> x;
ban[x] = 1;
}
auto cal = [](int x) {
int r = 0;
while (x) {
r += x % 10;
x /= 10;
}
return r;
};
int cs = cal(ss);
auto add = [&](int i, int t) {
if (t % 2 != cs % 2) return;
for (int j = 1; j < n; j *= 10) {
// 往上移动一位
if (i + j < n && cal(i + j) == t + 1) {
if(!ban[i + j]) g.addEdge(i, i + j, 1);
} else {
if(!ban[i - 9 * j]) g.addEdge(i, i - 9 * j, 1);
}
// 往下移动一位
if (i >= j && cal(i - j) == t - 1) {
if (!ban[i - j]) g.addEdge(i, i - j, 1);
} else {
if (!ban[i + 9 * j]) g.addEdge(i, i + 9 * j, 1);
}
}
};
for (int i = 0; i < n; ++i) if (!ban[i]) {
if (cal(i) % 2 == cs % 2) g.addEdge(s, i, 1);
else g.addEdge(i, t, 1);
}
ban[ss] = 1;
for (int i = 0; i < n; ++i) if (!ban[i]) {
add(i, cal(i));
}
g.maxFlow(s, t);
ban[ss] = 0;
g.addEdge(s, ss, 1);
add(ss, cs);
return g.maxFlow(s, t);
}

int main() {
// freopen("C:\\Users\\dna049\\cf\\in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
std::cout << (solve() ? "Alice\n" : "Bob\n");
}
return 0;
}

gym 102832K:gcd once more

考虑 $\gcd(x, y) = x ^ y$。我总想把最终形式给搞出来(想太多,吃力不讨好),首先显然 $x \neq y$(不考虑 0),不妨设 $x > y$,那么 $x - y \geq \gcd(x - y, y) = \gcd(x, y) = x^y \geq x - y$。从而知道 $\gcd(x, y) = x^y = x - y$,然后我还一直想继续推,甚至猜想 $x = 2^k(2n + 1), y = 2^{k + 1} \cdot n$,浪费了特别多的时间,其实此时显然可以枚举 gcd 的值在 $O(n \log n)$ 把所有情况找出来嘛。后面就是并查集常规操作了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const int N = 2e5 + 2;
std::vector<int> bad[N];
void init() {
for (int i = 1; i < N; ++i) {
for (int j = i * 2; j < N; j += i) if ((j ^ i) == j - i){
bad[j - i].emplace_back(j);
bad[j].emplace_back(j - i);
}
}
for (int i = 1; i < N; ++i) std::sort(bad[i].begin(), bad[i].end());
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
init();
int n, m;
std::cin >> n >> m;
std::vector<int> p(n + m + 1), a(n + m + 1);
std::iota(p.begin(), p.end(), 0);
std::vector<std::map<int, int>> mp(n + m + 1);
for (int i = 1; i <= n; ++i) {
std::cin >> a[i];
mp[i].insert({a[i], 1});
}
std::function<int(int)> find = [&](int x) ->int {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
};
LL ans = 0;
auto merge = [&](int x, int y) {
int fx = find(x), fy = find(y);
if (fx == fy) return;
if (mp[fx].size() < mp[fy].size()) std::swap(fx, fy);
p[fy] = fx;
for (auto [v, c] : mp[fy]) {
for (auto t : bad[v]) if (mp[fx].count(t)) {
ans += LL(mp[fx][t]) * c;
}
}
for (auto [v, c] : mp[fy]) {
mp[fx][v] += c;
}
};
auto change = [&](int x, int y) {
int fx = find(x);
auto &it = mp[fx];
for (auto t : bad[a[x]]) if (it.count(t)) {
ans -= it[t];
}
--it[a[x]];
for (auto t : bad[y]) if (it.count(t)) {
ans += it[t];
}
++it[y];
a[x] = y;
};
for (int i = 0; i < m; ++i) {
int op, x, y;
std::cin >> op >> x >> y;
if (op == 1) {
a[x] = y;
mp[x].insert({y, 1});
} else if (op == 2) {
merge(x, y);
} else if (a[x] != y) {
change(x, y);
}
std::cout << ans << "\n";
}
return 0;
}

1313C2:经典问题:单调栈优化

首先最优答案肯定时先递增后递减的。相当于有一个制高点,枚举制高点,自然有 $O(n^2)$ 的算法。但是利用单调栈可以优化到 $O(n)$。设 l[i], r[i] 分别表示以 i 为最高点的(前,后)缀和最大值。只讨论前缀,那么求 l[i] 自然时往前找比它小的,然后继承比的小的答案了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
auto f = [&]() -> std::vector<int> {
std::vector<int> pre(n, -1);
std::stack<int> Q;
for (int i = 0; i < n; ++i) {
while (!Q.empty() && a[Q.top()] >= a[i]) Q.pop();
if (!Q.empty()) pre[i] = Q.top();
Q.push(i);
}
return pre;
};
auto g = [&]() -> std::vector<int> {
std::vector<int> nxt(n, n);
std::stack<int> Q;
for (int i = n - 1; i >= 0; --i) {
while (!Q.empty() && a[Q.top()] >= a[i]) Q.pop();
if (!Q.empty()) nxt[i] = Q.top();
Q.push(i);
}
return nxt;
};
auto pre = f(), suf = g();
std::vector<LL> b(n), c(n);
for (int i = 0; i < n; ++i) {
b[i] = LL(i - pre[i]) * a[i] + (pre[i] == -1 ? 0 : b[pre[i]]);
}
for (int i = n - 1; i >= 0; --i) {
c[i] = LL(suf[i] - i) * a[i] + (suf[i] == n ? 0 : c[suf[i]]);
}
LL r = 0;
int id = 0;
for (int i = 0; i < n; ++i) {
if (b[i] + c[i] - a[i] > r) {
r = b[i] + c[i] - a[i];
id = i;
}
}
for (int i = id; i != -1; i = pre[i]) {
for (int j = i - 1; j != pre[i]; --j) a[j] = a[i];
}
for (int i = id; i != n; i = suf[i]) {
for (int j = i + 1; j != suf[i]; ++j) a[j] = a[i];
}
for (int i = 0; i < n; ++i) std::cout << a[i] << " \n"[i == n - 1];
return 0;
}

LOJ P1823:经典问题:问有多少对元素,它们之间没有比它们都大的元素

用单调栈存可以被当前位置的人看到的人的编号,显然是单调不增的(你回头一看,看到的人升高时单调不减的)。由于有身高相同的情况,所以需要合并相同身高。

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

izlyforever 2021/1/26 23:24:58
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::stack<std::pair<int, int>> S;
LL r = 0;
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
int cnt = 0;
while (!S.empty() && S.top().first <= x) {
if (S.top().first == x) {
cnt = S.top().second;
}
r += S.top().second;
S.pop();
}
if (!S.empty()) ++r;
S.push({x, ++cnt});
}
std::cout << r << "\n";
return 0;
}

1009F:长链剖分 dsu on tree

C++ 图论模板 长链剖分中有讲解。这个题重链剖分也可以写,但是要写成这样的(不需要编译器优化的版本):submission 105484073,更优雅更快(由于编译器优化)的写法:submission 105483361 会 RE。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

// 为了代码简洁,树的编号以 1 开始。
std::vector<int> dsuOnTree(std::vector<std::vector<int>> &e, int rt = 1) {
int n = e.size();
// 预处理出重儿子
std::vector<int> sz(n), son(n), dep(n);
dep[rt] = 0;
std::function<int(int, int)> pdfs = [&](int u, int fa) -> int {
sz[u] = 1;
for (auto v : e[u]) if (v != fa) {
dep[v] = dep[u] + 1;
sz[u] += pdfs(v, u);
if (sz[v] > sz[son[u]]) son[u] = v;
}
return sz[u];
};
std::vector<int> ans(n);
std::vector<std::map<int, int>> mp(n);
std::function<void(int, int)> dfs = [&](int u, int fa) -> void {
if (son[u] == 0) {
ans[u] = dep[u];
mp[u].insert({dep[u], 1});
return;
}
dfs(son[u], u);
std::swap(mp[son[u]], mp[u]);
ans[u] = ans[son[u]];
auto &mpu = mp[u];
int mx = mpu[ans[u]];
auto deal = [&](int x, int c) {
auto &it = mpu[x];
it += c;
if (it > mx || (it == mx && ans[u] >= x)) {
ans[u] = x;
mx = it;
}
};
deal(dep[u], 1);
for (auto v : e[u]) if (v != fa && v != son[u]) {
dfs(v, u);
for (auto [t, x] : mp[v]) {
deal(t, x);
}
}
};
pdfs(rt,0);
dfs(rt, 0);
for (int i = 1; i < n; ++i) ans[i] -= dep[i];
return ans;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::vector<int>> e(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
auto r = dsuOnTree(e);
for (int i = 1; i <= n; ++i) std::cout << r[i] << "\n";
return 0;
}

``` C++
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

// 为了代码简洁,树的编号以 1 开始。
std::vector<int> dsuOnTree(std::vector<std::vector<int>> &e, int rt = 1) {
int n = e.size();
// 预处理出重儿子
std::vector<int> sz(n), son(n);
std::function<void(int, int)> pdfs = [&](int u, int fa) -> void {
for (auto v : e[u]) if (v != fa) {
pdfs(v, u);
if (sz[v] > sz[son[u]]) son[u] = v;
}
sz[u] = sz[son[u]] + 1;
};
std::vector<int> ans(n);
std::function<std::vector<int>(int, int)> dfs = [&](int u, int fa) -> std::vector<int> {
if (son[u] == 0) {
ans[u] = 0;
return {1};
}
auto a = dfs(son[u], u);
ans[u] = ans[son[u]];
for (auto v : e[u]) if (v != fa && v != son[u]) {
auto tmp = dfs(v, u);
// 这里需要对齐
for (int ai = a.size() - 1, ti = tmp.size() - 1; ti >= 0; --ti, --ai) {
a[ai] += tmp[ti];
if (a[ai] > a[ans[u]] || (a[ai] == a[ans[u]] && ai > ans[u])) {
ans[u] = ai;
}
}
}
a.emplace_back(1);
if (a[ans[u]] == 1) ans[u] = sz[u] - 1;
return a;
};
pdfs(rt, 0);
dfs(rt, 0);
for (int i = 1; i < n; ++i) ans[i] = sz[i] - 1 - ans[i];
return ans;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::vector<int>> e(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
auto r = dsuOnTree(e);
for (int i = 1; i <= n; ++i) std::cout << r[i] << "\n";
return 0;
}

gym 102832F:dsu on tree

首先,我们可以枚举 lca(i, j),也就是说每一个节点都可以当其子树的 lca,它的两个子树中元素的 lca 必然是它。因此把这个答案算成是这个节点的答案。然后就是轻重链的问题了。注意到这里答案是异或值求和,那么我们可以诸位考虑即可。这里的写法跟 600E 的写法不一致。其实也可以写成一致的样子,600E 也可以写成我这个样子。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

class Node {
public:
inline static const int N = 18;
int a[N]{};
Node() {}
Node(int x) {
++a[0];
int now = 0;
while (x) {
a[++now] = x & 1;
x >>= 1;
}
}
Node operator+=(const Node &A) {
for (int i = 0; i < N; ++i) a[i] += A.a[i];
return *this;
}
};
LL deal(const Node & A, const Node & B) {
LL r = 0;
for (int i = 1; i < Node::N; ++i) {
r += (LL(A.a[0] - A.a[i]) * B.a[i] + LL(B.a[0] - B.a[i]) * A.a[i]) << (i - 1);
}
return r;
}

// 为了代码简洁,树的编号以 1 开始
LL dsuOnTree(std::vector<std::vector<int>> &e, std::vector<int> &a, int rt = 1) {
int n = a.size();
// 预处理出重儿子
std::vector<int> sz(n), son(n);
std::function<int(int, int)> pdfs = [&](int u, int fa) -> int {
sz[u] = 1;
for (auto v : e[u]) if (v != fa) {
sz[u] += pdfs(v, u);
if (sz[v] > sz[son[u]]) son[u] = v;
}
return sz[u];
};
std::vector<LL> ans(n);
std::function<std::unordered_map<int, Node>(int, int)> dfs = [&](int u, int fa) -> std::unordered_map<int, Node> {
if (son[u] == 0) return {{a[u], Node(u)}};
auto mp = dfs(son[u], u); // 这里开 O2 被编译器优化了,不然直接爆炸
mp[a[u]] += Node(u);
LL r = 0;
for (auto v : e[u]) if (v != fa && v != son[u]) {
auto tmp = dfs(v, u);
for (auto [i, x] : tmp) {
if (auto it = mp.find(i ^ a[u]); it != mp.end()) {
r += deal(it->second, x);
}
}
for (auto [i, x] : tmp) mp[i] += x;
}
ans[u] = r;
return mp;
};
pdfs(rt, rt);
dfs(rt, rt);
LL r = 0;
for (int i = 1; i < n; ++i) r += ans[i];
return r;
}

int main() {
// freopen("C:\\Users\\dna049\\cf\\in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; ++i) std::cin >> a[i];
std::vector<std::vector<int>> e(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
std::cout << dsuOnTree(e, a) << std::endl;
return 0;
}

600E:dsu on tree

不借鉴别人,自己独创的优秀写法

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

// 为了代码简洁,树的编号以 1 开始,参考:https://www.cnblogs.com/zwfymqz/p/9683124.html
std::vector<LL> dsuOnTree(std::vector<std::vector<int>> &e, std::vector<int> &a, int rt = 1) {
int n = a.size();
std::vector<int> sz(n), son(n), cnt(n);
std::vector<LL> ans(n);
// 预处理出重儿子
std::function<int(int, int)> pdfs = [&](int u, int fa) -> int {
sz[u] = 1;
for (auto v : e[u]) if (v != fa) {
sz[u] += pdfs(v, u);
if (sz[v] > sz[son[u]]) son[u] = v;
}
return sz[u];
};
// 这个函数具体问题具体分析
int mx = 0, Son = 0;
LL sm = 0;
std::function<void(int, int)> deal = [&](int u, int fa) -> void {
++cnt[a[u]];
if (cnt[a[u]] > mx) {
mx = cnt[a[u]];
sm = a[u];
} else if (cnt[a[u]] == mx) {
sm += a[u];
}
for (auto v : e[u]) if (v != fa && v != Son) {
deal(v, u);
}
};
std::function<void(int, int)> del = [&](int u, int fa) -> void {
--cnt[a[u]];
for (auto v : e[u]) if (v != fa) del(v, u);
};
std::function<void(int, int, bool)> dfs = [&](int u, int fa, bool save) -> void {
for (auto v : e[u]) if (v != fa && v != son[u]) {
dfs(v, u, 0); // 先计算轻边贡献,但最终要消除影响,防止影响轻边
}
if (son[u]) dfs(son[u], u, 1); // 统计重儿子的贡献,但不消除影响
Son = son[u];
deal(u, fa); // 暴力处理除重儿子外的贡献
Son = 0;
ans[u] = sm;
if (!save) {
del(u, fa);
sm = 0;
mx = 0;
}
};
pdfs(rt, rt);
dfs(rt, rt, 1);
return ans;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; ++i) std::cin >> a[i];
std::vector<std::vector<int>> e(n + 1);
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
auto r = dsuOnTree(e, a);
for (int i = 1; i <= n; ++i) std::cout << r[i] << " \n"[i == n];
return 0;
}

gym 102832D:无意义的序列

首先 c = 0 时特判,$c \geq 1$ 时,观察到 $a_n = c^{bit}$, bit 为 n 的二进制中 1 的个数。然后我们只需看小于 10…0(k 个 0) 时的答案即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const LL M = 1e9 + 7;
LL powMod(LL x, LL n) {
LL r = 1;
while (n) {
if (n & 1) r = r * x % M;
n >>= 1; x = x * x % M;
}
return r;
}
const int N = 3e5 + 2;
LL fac[N], ifac[N];
void init() {
fac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = fac[i - 1] * i % M;
ifac[N - 1] = powMod(fac[N - 1], M - 2);
for (int i = N - 1; i; --i) ifac[i - 1] = ifac[i] * i % M;
}
LL binom(int n, int k) {
//if (n < k || n < 0) return 0;
return fac[n] * ifac[k] % M * ifac[n - k] % M;
}

LL solve(int n, int a, int c) {
LL r = 0;
for (int i = 0; i <= n; ++i) {
r += binom(n, i) * powMod(c, i + a) % M;
}
return r % M;
}
int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::string s;
int c;
std::cin >> s >> c;
if (c == 0) {
std::cout << "1\n";
return 0;
}
init();
LL r = 0;
int a = 0;
for (int i = 0; i < s.size(); ++i) if (s[i] == '1') {
r += solve(s.size() - i - 1, a++, c);
}
r += powMod(c, a);
std::cout << r % M << std::endl;
return 0;
}

AtCoder ABC189F

从 0 位置出发,走到大于等于 n 的位置结束,每次平均概率在 [1, m] 步中选择步长来走,有 k 个坑,走到坑就回到起点 0。问结束前步数的期望是多少,如果无法结束就输出 -1。

做法:首先如果有连续 m 个坑(很好判断),必然无法结束,否则可以结束,我们设 dp[i] 表示从 i 出发的答案。显然 $dp[i] = 0, i \geq n$,我们从后往前跑,显然有状态转移,如果 i 位置有坑,那么 $dp[i] = dp[0]$, 否则 $dp[i] = (dp[i + 1] + \cdots dp[i + m]) / m + 1$。这个后缀和,我们可以用个变量记录下来。因此 所有的 dp[i] 都是一个 $a + b dp[0]$ 的形式,然后到最后有 $dp[0] = a + b dp[0]$ 从而就求得了结果。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
// freopen("C:\\Users\\dna049\\cf\\in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, k;
std::cin >> n >> m >> k;
std::vector<int> a(k);
for (auto &x: a) std::cin >> x;
for (int i = m - 1; i < k; ++i) if (a[i] - a[i - m + 1] == m - 1) {
std::cout << "-1\n";
return 0;
}
std::vector<double> b(n + m), c(n + m);
double sb = 0, sc = 0;
for (int i = n - 1; i >= 0; --i) {
if (a.size() && i == a.back()) {
b[i] = 0;
c[i] = 1;
a.pop_back();
} else {
b[i] = sb / m + 1;
c[i] = sc / m;
}
sb += b[i] - b[i + m];
sc += c[i] - c[i + m];
}
std::cout.precision(8);
std::cout << std::fixed << b[0] / (1 - c[0]) << std::endl;
return 0;
}

gym 102940H

长为 k 取值在 [1, n] 且满足 $a_i \mid a_{i + 1}$ 的序列有多少个。

我一开始以为跟 n 的素因子有关,后来发现没法直接推公式,然后发现是一个 dp 问题,令 dp[k][x] 表示长为 k 满足 $a_i \mid a_{i + 1}$ 且每项都是 x 的因子的序列个数。答案必然就是 $\sum_{x = 1}^n dp[k - 1][x]$。复杂度 $O(k n \log n)$

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const int M = 1e9 + 7;
int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<std::vector<int>> dp(k, std::vector<int>(n + 1));
for (int i = 1; i <= n; ++i) dp[0][i] = 1;
for (int i = 1; i < k; ++i) {
for (int j = 1; j <= n; ++j) {
for (int t = j; t <= n; t += j) {
(dp[i][t] += dp[i - 1][j]) %= M;
}
}
}
int r = 0;
for (int i = 1; i <= n; ++i) (r += dp[k - 1][i]) %= M;
std::cout << r << std::endl;
return 0;
}

1474D:相连同时减一

题意:每次可以同时使得相连两个数减 1,问是否能使得数组变成全 0。这个问题相当简单,因此换成,能否最多互换相连两个的值,使得原问题成立。

做法:原问题做法就是从左到右依次跑,如果跑出负数就不行,跑到最后不是 0 也不行。然后一开始我想错了,吃了两次 WA 之后,发现互换相邻两个之后原问题成立的前提是,从左到右跑和从右到左跑都不会出现负数。因此就保存左右两边跑的结果,然后只需考虑 4 个数的时候是否对就可以了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
auto check = [](std::vector<int> a) {
int now = 0;
for (auto x : a) {
now = x - now;
if (now < 0) return false;
}
return now == 0;
};
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
std::vector<int> b(n + 1, -1);
b[0] = 0;
for (int i = 0; i < n && b[i] >= 0; ++i) {
b[i + 1] = a[i] - b[i];
}
if (b[n] == 0) {
std::cout << "YES\n";
continue;
}
bool flag = false;
int now = 0;
for (int i = n - 1; i > 0 && now >= 0; --i) {
if (b[i - 1] >= 0 && check(std::vector<int>({b[i - 1], a[i], a[i - 1], now}))) {
flag = true;
break;
}
now = a[i] - now;
}
std::cout << (flag ? "YES\n" : "NO\n");
}
return 0;
}

1474C:乱搞题

一开始思路不清晰就写代码,写着发现有问题,被卡了挺长时间,导致 D 题差最后 10 分钟没有 debug 出一个小错误。

做法:从最大的开始找,然后删除对应的节点即可。初始值 x 必然是 a 中最大值和另一个值的和。分析好问题后再选取合适的 STL,我用的是 map,很多人用的是 multiset

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
auto f = [](std::map<int, int> mp, int x) {
std::vector<std::pair<int, int>> r;
while (!mp.empty()) {
auto it = mp.rbegin();
int u = it->first;
if (--mp[u] == 0) mp.erase(u);
if (mp.find(x - u) == mp.end()) return std::vector<std::pair<int, int>>();
r.emplace_back(x - u, u);
if (--mp[x - u] == 0) mp.erase(x - u);
x = u;
}
return r;
};
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::map<int, int> mp;
for (int i = 0, x; i < 2 * n; ++i) {
std::cin >> x;
++mp[x];
}
int x;
std::vector<std::pair<int, int>> r;
int t = mp.rbegin()->first;
for (auto it = mp.begin(); it != mp.end(); ++it) {
x = t + it->first;
r = f(mp, x);
if (r.size()) break;
}
if (r.size()) {
std::cout << "YES\n";
std::cout << x << "\n";
for (auto [x, y] : r) std::cout << x << " " << y << "\n";
} else std::cout << "NO\n";
}
return 0;
}

1473F:经典最大流(最小割)问题

题意:给定长为 n($1 \leq n \leq 3000$) 的数组 a($1 \leq a_i \leq 100$), b($-10^5 \leq b_i \leq 10^5$),求 $\displaystyle \max_{i \in S} b_i$,其中集合 $S$ 满足若 $i \in S$,则任意 $0 \leq j < i$, 若 $a_i \equiv 0 \mod a_j$,那么 $j$ 也在 $S$ 中。

做法:设 $s = n$ 为源点,$t = n + 1$ 为汇点,如果 $b_i > 0$(则称 i 为正点,否则为负点),那么我们从源点 $s$ 到 $i$ 建一个容量为 $b_i$ 的边,反之我们就从 $i$ 到 $s$ 建一个容量为 $-b_i$ 的边。如果 $j < i$ 满足 $a_i \equiv 0 \mod a_j$,那么从 $j$ 到 $i$ 建一个容量为无穷大的边(一个必要的优化,直接这样建图,边太多了,根据这个性质的传递性,我们不妨找最后一个值为 $a_j$ 的点和 $i$ 相连)。
我们不妨先把所有正点全部放在 $S$ 中,然后求最小割即可,注意到满足性质的边容量是无限大的,因此我们必然会将这样的两个节点放在一起!

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
96
97
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

class Dinic {
int n;
// e[i] 表示第 i 条边的终点和容量,注意存边的时候 e[i ^ 1] 是 e[i] 的反向边。
// g[u] 存的是所有以 u 为起点的边,这就很像链式前向星的做法
std::vector<std::pair<int, int>> e;
std::vector<std::vector<int>> g;
// cur[u] 表示以 u 为起点当前没被增广过的边
std::vector<int> cur, h;
// h[u] 表示 bfs 从 s 到 u 的距离,如果找到了 t,那么就说明找到了增广路。
bool bfs(int s, int t) {
h.assign(n, -1);
std::queue<int> Q;
h[s] = 0;
Q.push(s);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for (auto i : g[u]) {
auto [v, c] = e[i];
if (c > 0 && h[v] == -1) {
h[v] = h[u] + 1;
if (v == t) return true;
Q.push(v);
}
}
}
return false;
}
// f 表示从 u 点出发拥有的最大流量,输出的是 u 到 t 的最大流量
int dfs(int u, int t, int f) {
if (u == t) return f;
int r = f;
for (int &i = cur[u]; i < g[u].size(); ++i) {
int j = g[u][i];
auto [v, c] = e[j];
if (c > 0 && h[v] == h[u] + 1) {
int a = dfs(v, t, std::min(r, c));
e[j].second -= a;
e[j ^ 1].second += a;
r -= a;
if (r == 0) return f;
}
}
return f - r;
}
public:
Dinic(int _n) : n(_n), g(_n) {}
void addEdge(int u, int v, int c) {
g[u].emplace_back(e.size());
e.emplace_back(v, c);
g[v].emplace_back(e.size());
e.emplace_back(u, 0);
}
int maxFlow(int s, int t) {
int r = 0;
while (bfs(s, t)) {
cur.assign(n, 0);
r += dfs(s, t, INT_MAX);
}
return r;
}
};

int main() {
// freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n), b(n);
for (auto &x : a) std::cin >> x;
for (auto &x : b) std::cin >> x;
std::vector<int> last(101, -1);
Dinic g(n + 2);
int r = 0;
for (int i = 0; i < n; ++i) {
if (b[i] > 0) {
r += b[i];
g.addEdge(n, i, b[i]);
} else {
g.addEdge(i, n + 1, -b[i]);
}
for (int j = 1; j <= a[i]; ++j) {
if (a[i] % j == 0 && last[j] != -1) {
g.addEdge(i, last[j], INT_MAX);
}
}
last[a[i]] = i;
}
r -= g.maxFlow(n, n + 1);
std::cout << r << std::endl;
return 0;
}

1473D:经典前缀后缀

前缀和的历史最大值和历史最小值是特别好求的。后缀和的呢,却不那么显然。考虑后缀的时候,我们实际上要考虑后缀对最后结果的贡献。比如最大值,我们需要看当前后缀是否大于 0,如果小于等于 0 就直接抛弃重新开始,否则就继续保存。最小值同理。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, m;
std::string s;
std::cin >> n >> m >> s;
std::vector<int> c(n + 2);
std::vector<std::pair<int, int>> a(n + 2), b(n + 2);
int now = 0, mx = 0, mn = 0;
for (int i = 1; i <= n; ++i) {
if (s[i - 1] == '+') {
mx = std::max(mx, ++now);
} else {
mn = std::min(mn, --now);
}
a[i] = {mn, mx};
c[i] = now;
}
int np = 0, nq = 0;
for (int i = n; i > 0; --i) {
if (s[i - 1] == '+') {
++np;
nq = std::min(++nq, 0);
} else {
--nq;
np = std::max(--np, 0);
}
b[i] = {nq, np};
}
while (m--) {
int l, r;
std::cin >> l >> r;
std::cout << std::max(a[l - 1].second, c[l - 1] + b[r + 1].second)
- std::min(a[l - 1].first, c[l - 1] + b[r + 1].first) + 1 << std::endl;
}
}
return 0;
}

1473E:经典最短路,去掉一个最长路,加上一个最短路

一条路径的权值定义为 $\sum w_{e_i} - \max e_i + \min e_i$,显然这等价于 $\min \sum (w_{e_i}) - w_{e_j} + w_{e_k}$。因此我们可以建图:比如原始边为 (u, v, w), 一个节点到了 4u 表示是原始的长度, 4u + 1 表示减去了某个边,4u + 2 表示加上了某条边,4u + 3 表示既加了也减了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

using edge = std::vector<std::vector<std::pair<int, int>>>;
std::vector<LL> Dijkstra(int s, const edge &e) {
std::priority_queue<std::pair<LL, int>> h;
std::vector<LL> dist(e.size());
std::vector<int> vis(e.size());
dist[s] = 0;
h.push({0, s});
while (!h.empty()) {
auto [d, u] = h.top();
h.pop();
if (vis[u]) continue;
vis[u] = 1;
dist[u] = -d;
for (auto [v, w] : e[u]) h.emplace(d - w, v);
}
return dist;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
edge e(4 * n);
for (int i = 0; i < m; ++i) {
int u, v, w;
std::cin >> u >> v >> w;
--u; --v;
for (int t = 0; t < 2; ++t) {
for (int j = 0; j < 4; ++j) e[4 * u + j].emplace_back(4 * v + j, w);
e[4 * u].emplace_back(4 * v + 1, 0);
e[4 * u].emplace_back(4 * v + 2, 2 * w);
e[4 * u].emplace_back(4 * v + 3, w);
e[4 * u + 1].emplace_back(4 * v + 3, 2 * w);
e[4 * u + 2].emplace_back(4 * v + 3, 0);
std::swap(u, v);
}
}
auto dist = Dijkstra(0, e);
for (int i = 7; i < 4 * n; i += 4) std::cout << dist[i] << " ";
std::cout << "\n";
return 0;
}

也可以不建成上述图,按照原始图建图,操作的时候再也可以,本质上一致更节省空间,代码稍微复杂一点。

AtCoder arc111B:经典 2 选 1

大致 有 $n$ 个盒子,每个盒子有两个数,从中取去一个数,问最多可以取多少个不同的数。

数字为节点,盒子中的两个数连边(注意可能有重边),那个连通分支是树,那么答案就是连通分支节点数减 1,否则就是连通分支节点数。(树的情况容易证明,非树的情况总可以删边,删成只有树再多一条边的情况,然后也容易证明)

AtCoder arc111E

教程

1467C:类似于把 a, b 变成 a - b 问题(代码自解释)

题目中 3 个袋子可以换成 $m(m \geq 3)$ 个袋子。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n[3]{};
std::cin >> n[0] >> n[1] >> n[2];
LL r = 0;
std::vector<std::vector<int>> a(3);
std::vector<int> c;
for (int i = 0; i < 3; ++i) {
for (int j = 0, x; j < n[i]; ++j) {
std::cin >> x;
a[i].emplace_back(x);
r += x;
}
}
std::vector<int> b;
for (int i = 0; i < 3; ++i) {
b.emplace_back(*std::min_element(a[i].begin(), a[i].end()));
}
std::sort(b.begin(), b.end());
LL ans = r - b[0] * 2 - b[1] * 2;
for (int i = 0; i < 3; ++i) {
LL tmp = r;
for (int j = 0; j < n[i]; ++j) tmp -= a[i][j] * 2;
ans = std::max(ans, tmp);
}
std::cout << ans << std::endl;
return 0;
}

1471D:GCD 问题

题意:我们称 $a, b$ 相邻,如果 $\lcm(a, b)/gcd(a, b)$ 是平方数,这当且仅当 $ab$ 是平方数。我们定义 $f(n)$ 为 $n$ 的素因子的积,那么 $a, b$ 相邻,当且仅当 $f(a) = f(b)$。这样就好了呀。由于数据范围在 $1e6$ 之间,所以可以预处理以下就可以了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

const int N = 1e6 + 10086;
int sp[N], p[N], f[N];
int spf(){
int cnt = 1;
p[1] = 2;
for (int i = 2; i < N; i += 2) sp[i] = 2;
for (int i = 1; i < N; i += 2) sp[i] = i;
for (int i = 3; i < N; i += 2) {
if (sp[i] == i) p[++cnt] = i;
for (int j = 1; j <= cnt && p[j] <= sp[i] && i * p[j] < N; ++j) { //防止乘法溢出
sp[i * p[j]] = p[j]; // 注意到sp只被赋值一次
}
}
f[1] = 1;
for (int i = 2; i < N; ++i) {
int pi = i / sp[i];
if (pi % sp[i] == 0) f[i] = f[pi / sp[i]];
else f[i] = f[pi] * sp[i];
}
return cnt;
}

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
spf();
while (cas--) {
int n;
std::cin >> n;
std::map<int, int> mp;
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
++mp[f[x]];
}
int r0 = INT_MIN, now = mp[1];
for (auto [u, v] : mp) {
r0 = std::max(r0, v);
if (u != 1 && v % 2 == 0) now += v;
}
int r1 = std::max(r0, now);
int q;
std::cin >> q;
while (q--) {
LL w;
std::cin >> w;
std::cout << (w == 0 ? r0 : r1) << "\n";
}
}
return 0;
}

上面做法还是太慢了!注意到 $f(n)$ 表示 $n$ 的最小 “无平方因子” 的因子。因此可以用平方数预处理,可用下面代码加速

1
2
3
4
5
6
void init() {
for (int i = 1; i * i < N; ++i) {
int ii = i * i, cur = 0;
for (int j = ii; j < N; j += ii) f[j] = ++cur;
}
}

1471F: 图论染色问题

比赛时,读题没读懂要干嘛。

用 01 对连通图染色,使得每个 0 周围全是 1,每个 1 周围都存在一个 0. 按照题解的说法:随便选择一点置 0,然后把它周围全部染色为 1,然后找和 1 相邻的没被染色的其中任意一个染色为 0,然后把 0 周围全部染色为 1,一直继续下去。

但是上述做法代码可能写的比较别扭,因此我们可以用队列,先随便选择一点置 0,放进队列中。在队列中的被染色为 1 的是真 1,染色为 0 的表示它和某个染色为 1 的节点相连。出队���后染色为 1 的是真的 1。队首的被染色为 0,那就把和它相邻的没被染色的变成 1 放在队列中,队首的被染色为 1,那就把它相邻的全变成 0(不管有没有被染色过),若没被染色就丢进队列中。

注意到:被染色为 0 后,染色不会变化。出队列的 1 周围的染色全为 0,被染色为 0,必然是某个出了队列的 1 帮它染的。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> e(n);
for (int i = 0, u, v; i < m; ++i) {
std::cin >> u >> v;
--u;
--v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
std::vector<int> val(n, -1);
val[0] = 0;
std::queue<int> Q;
Q.push(0);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
if (val[u] == 0) {
for (auto v : e[u]) {
if (val[v] == -1) {
val[v] = 1;
Q.push(v);
} else val[v] = 1;
}
} else {
for (auto v : e[u]) if (val[v] == -1) {
val[v] = 0;
Q.push(v);
}
}
}
if (*std::min_element(val.begin(), val.end()) == -1) {
std::cout << "NO\n";
} else {
std::cout << "YES\n";
std::vector<int> r;
for (int i = 0; i < n; ++i) {
if (val[i] == 0) r.emplace_back(i);
}
std::cout << r.size() << "\n";
for (auto i : r) std::cout << i + 1 << " ";
std::cout << "\n";
}
}
return 0;
}

1466A:线段上 $n$ 个点,两两距离差值的所有可能个数

$n^2$ 的算法是显然的。

  1. 利用 Bitset,也是 $n^2$ 的 dp,然后用 bitset 存储数据,就可以达到 $n^2/64$ 的复杂度啦。
  2. 如果距离的最大值区间为 $N$,那么 $\sum x^{a_i} \sum x^(N - a_i)$ 中非负系数个数就是答案(所以有 $O(N\log N)$ 的做法。

1466E:求和

显然就是固定 $j$,每位每位的求和即可。

1466F:$\mathbb{Z}_2$ 上 $m$ 维向量线性无关组

注意到题目中至多两个位置非零,因此就可以用 并查集(一般情况倒是不知道有啥好办法)。注意到官方题解,可以多加一个维度,使得有每次正好有两个位置非零。

1465D:观察结果题

题意:给定由 0, 1, ? 构成的字符串,将 ? 变成 0 或 1,使得 01 字符和 10 字符权值和最小。(其中,01 权值为 x, 10 权值为 y)

不妨假设 $x \leq y$, 否则将字符串反序(x, y 互换)

做法:首先不考虑 ?,此时我们可以通过当前位置为 0(看前方 1 的个数),当前位置为 1(看前置 0 的个数)得到基础权值 W,然后注意到 01 的权值低于 10 的权值,因此当 m 个 ? 替换成 p 个 1 和 m - p 个 0 时候,把 0,放在最前面会让权值最小,因此我们可以考虑前缀问号给 0,后缀问号为 1 给答案的贡献。然后再加上基础权值 W 再加上 m(m-p)x

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::string a;
LL x, y;
std::cin >> a >> x >> y;
if (x > y) {
std::swap(x, y);
std::reverse(a.begin(), a.end());
}
int n = a.size();
std::vector<int> p[2], s[2];
p[0].resize(n + 1);
p[1].resize(n + 1);
s[0].resize(n + 1);
s[1].resize(n + 1);
for (int i = 0; i < n; ++i) {
p[0][i + 1] = p[0][i];
p[1][i + 1] = p[1][i];
if (a[i] != '?') ++p[a[i] - '0'][i + 1];
}
for (int i = n - 1; i >= 0; --i) {
s[0][i] = s[0][i + 1];
s[1][i] = s[1][i + 1];
if (a[i] != '?') ++s[a[i] - '0'][i];
}
LL ord = 0;
for (int i = 0; i < n; ++i) {
if (a[i] == '0') ord += p[1][i] * y;
if (a[i] == '1') ord += p[0][i] * x;
}
std::vector<LL> pre(n + 1), suf(n + 1);
for (int i = 0; i < n; ++i) {
pre[i + 1] = pre[i];
if (a[i] == '?') pre[i + 1] += s[1][i] * x + p[1][i] * y;
}
for (int i = n - 1; i >= 0; --i) {
suf[i] = suf[i + 1];
if (a[i] == '?') suf[i] += s[0][i] * y + p[0][i] * x;
}
int cnt = std::count(a.begin(), a.end(), '?'), tnc = 0;
LL r = ord + suf[0];
for (int i = 0; i < n; ++i) if (a[i] == '?') {
++tnc;
r = std::min(r, ord + pre[i + 1] + suf[i + 1] + x * (cnt - tnc) * tnc);
}
std::cout << r << std::endl;
return 0;
}

1465E:观察题

官方题解 清晰明了

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
LL s;
std::string a;
std::cin >> n >> s >> a;
s -= 1 << (a.back() - 'a');
a.pop_back();
s += 1 << (a.back() - 'a');
a.pop_back();
s = abs(s);
LL cnt[26]{};
for (auto x : a) ++cnt[x - 'a'];
for (int i = 25; i >= 0; --i) {
if ((s >> i) >= cnt[i]) s -= cnt[i] << i;
else {
int t = cnt[i] - (s >> i);
s -= (s >> i) << i;
if (t % 2) s = (1 << i) - s;
}
}
std::cout << (s == 0 ? "Yes" : "No") << std::endl;
return 0;
}

用桶排序和取正处理,直接起飞,复杂度骤降为 $O(n)$。

1450H:圆上配对问题

官方题解 属实精彩。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
const LL M = 998244353;

LL powMod(LL x, LL n) {
LL r = 1;
while (n) {
if (n & 1) r = r * x % M;
n >>= 1; x = x * x % M;
}
return r;
}

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::string s;
std::cin >> n >> m >> s;

std::vector<LL> fac(n + 1), ifac(n + 1);
fac[0] = 1;
for (int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i % M;
ifac[n] = powMod(fac[n], M - 2);
for (int i = n; i >= 1; --i) ifac[i - 1] = ifac[i] * i % M;
auto binom = [&](int n, int k) {
return fac[n] * ifac[n - k] % M * ifac[k] % M;
};

int B[2] = {}, res[2] = {};
for (int i = 0; i < n; ++i) {
if (s[i] == '?') ++res[i % 2];
else if (s[i] == 'b') ++B[i % 2];
}
int F = res[0] + res[1], x = B[1] + res[1] - B[0];

LL r = 0;
for (int i = (x + n) % 2; i <= F; i += 2) {
(r += abs(x - i) * binom(F, i)) %= M;
}
r = r * powMod(2LL, F * (M - 2) % (M - 1)) % M;
std::cout << r << std::endl;
return 0;
}

1453D:简单概率题

这么简答的概率题,我竟然做了一个小时,被弱智的错误理解题意搞的头皮发麻!

题意:对一个长度为 $n$ 的 0-1 序列(首位为 1),0 表示不存档 1 表示存档,如果我们在第 i 关打赢了,那么我们进入第 i + 1 关,否则我们回到最近的一次存档处。那么战斗次数的期望就确定的。现在问题是期望值 $k$,能否给一个 $0-1$ 序列。

首先如果 $n = 1$,那么此时期望为 $\displaystyle \sum_{i = 1}^n \frac{i}{2^i} = 2$. 注意到如果 a[i] = 1,一旦到达 i 位置,那么期望与前面的部分就无关了。所以其实我们只需考虑序列 10...0。记期望为 $p_n$。那么

化简可得 $p_n = 2 (2^n - 1)$. 因此只要 k 是 2 的倍数,必然序列存在。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
LL n;
std::cin >> n;
if (n & 1) {
std::cout << -1 << std::endl;
continue;
}
n /= 2;
std::vector<int> a;
while (n) {
int x = 1;
while ((1LL << x) <= n + 1) ++x;
--x;
n -= (1LL << x) - 1;
a.emplace_back(1);
for (int i = 1; i < x; ++i) a.emplace_back(0);
}
std::cout << a.size() << std::endl;
for (auto x : a) std::cout << x << " ";
std::cout << std::endl;
}
return 0;
}

1455B:简单被卡题

题意:第 $k$ 次移动时,可以往前移动 $k$ 个位置,或往后移动一个位置,从 0 到 $n$ 最少多少步完成。

一开始在想用 bfs 或者 dfs 或者 dp 来做,没有第一时间搞贪心。写完 bfs 之后,跑不动吐了。此题最多向后走一个位置即可。找到最小的 x 满足 $1 + \cdots x \geq n$,如果 $1 + \cdots x = n + 1$,那么答案就是 $x + 1$,否则为 $x$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
const int N = 1e6 + 2;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int x;
std::cin >> x;
int r = 1;
while (r * (r + 1) / 2 < x) ++ r;
if (r * (r + 1) / 2 == x + 1) ++r;
std::cout << r << std::endl;
}
return 0;
}

1455E:好玩的问题

给定四个不同的点,将它们分别移动,使得称为一个正方形,且边分别和轴平行。左上角,右上角,左下角,右下角四个点明确之后,那么坐标分开讨论。利用一个事实,就是线段上,距离端点长度之和为常量。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n = 4;
std::vector<pll> a(n);
for (auto &[x, y] : a) std::cin >> x >> y;
LL ans = INT64_MAX;
std::sort(a.begin(), a.end());
do {
LL sum = 0;
// 0 3
// 1 2
sum += abs(a[0].first - a[1].first);
sum += abs(a[2].first - a[3].first);
sum += abs(a[0].second - a[3].second);
sum += abs(a[1].second - a[2].second);
LL xr = std::max(a[3].first, a[2].first) - std::min(a[1].first, a[0].first);
LL xl = std::min(a[3].first, a[2].first) - std::max(a[1].first, a[0].first);
LL yr = std::max(a[0].second, a[3].second) - std::min(a[1].second, a[2].second);
LL yl = std::min(a[0].second, a[3].second) - std::max(a[1].second, a[2].second);
ans = std::min(ans, sum + 2 * std::max(0LL, std::max(xl, yl) - std::min(xr, yr)));
} while (std::next_permutation(a.begin(), a.end()));
std::cout << ans << std::endl;
}
return 0;
}

本场 1456 Div1, 我只能做 AB 两题,但是我可以做的快,做的优雅的,可是我没有!

1456A:经典 DP

题意:给定 0-1 字符串,需要让 $p, p + k, \cdots$ 位置都变成 1,每变一个位代价是 x,或者删除最开始的字符,这样做代价是 y。

做法:我们可以把字符串反过来,然后答案就是 a[n - p],状态转移:不删字符的情况下,a[i] 为 $s[i], s[i - k], \cdots$ 中 0 的个数。$a[i] = \min_{1 \leq t \leq i}(a[i], a[i - t] + t * y)$,所以我们可以将 a[i] - iy 添加到 set 中,然后最小值加上 当前的 iy 就是真实的最小值。

很早就想到了做法,实现的时候写的太急了,分析不过细致,把自己整吐了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, p, k, x, y;
std::string s;
std::cin >> n >> p >> k >> s >> x >> y;
std::reverse(s.begin(),s.end());
std::vector<int> a(n), cnt(k);
std::set<int> S;
for (int i = 0; i < n; ++i) {
if (s[i] == '0') ++cnt[i % k];
a[i] = cnt[i % k] * x;
if (!S.empty()) a[i] = std::min(a[i], *S.begin() + i * y);
S.insert(a[i] - i * y);
}
std::cout << a[n - p] << std::endl;
}
return 0;
}

1456B:XOR 问题

题意:给定一个非降的序列,可以将相邻的两个变成它们的异或值,能否在最小的步数上,将这个序列不满足非降条件。

做法:注意到,如果有三个相邻的数最高位一致,那么答案必然是 1,因此本质上我们只需考虑 n = 60 的情况,所以,随便写就能过(所以我写的特别随便,然后被人 hack 了,我真的服了自己!)

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
int solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
for (int i = 0; i + 2 < n; ++i) {
if ((a[i] ^ a[i + 1]) > a[i + 2]) return 1;
if (a[i] > (a[i + 1] ^ a[i + 2])) return 1;
}
int ans = INT_MAX;
for (int i = 1; i < n; ++i) {
std::vector<int> b, c;
int nb = 0;
for (int j = i - 1; j >= 0; --j) {
nb ^= a[j];
b.emplace_back(nb);
}
int nc = 0;
for (int j = i; j < n; ++j) {
nc ^= a[j];
c.emplace_back(nc);
}
for (int j = 0; j < b.size(); ++j) {
for (int k = 0; k < c.size(); ++k) if (b[j] > c[k]) {
ans = std::min(ans, j + k);
}
}
}
return ans == INT_MAX ? -1 : ans;
}
int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout << solve() << std::endl;
return 0;
}

AtCoder abc184F:假 0-1 背包,Meet in Middle

题意:给定序列,求选择其中部分,使得它们和最大且不超过 t,这不就是 0-1 背包吗?但是数据范围 $0 \leq n \leq 40, 0 \leq a_i, t \leq 10^9$,此题即使是多重背包,也可以用下面各种方法来做

  1. Meet in Middle,但是实现的时候可以有以下几种实现细节:
    • 用是 set 或 unordered set 存和更新,然后用双指针,整体复杂度 $O(n 2 ^{\frac{n}{2}})$
    • 用 Vector 存,之后排序(只需排序一个),然后用 lower_bounded 查找,复杂度同理
    • 用 Vector 存,保持有序,最后用双指针,复杂度 $O(2 ^{\frac{n}{2}})$
  2. 先 dfs 找到一个较好的解,然后每次更新解,用来剪枝。
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
// 有序 vector + 双指针,复杂度 $O(2 ^{\frac{n}{2}})$
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int solve() {
int n, t;
std::cin >> n >> t;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
std::sort(a.rbegin(), a.rend());
auto merge = [&](std::vector<int>& a, int x) {
std::vector<int> b;
for (int i = 0, j = 0; i < a.size(); ++i) {
while (j < a.size() && a[i] < a[j] + x) {
if (a[j] + x <= t) b.emplace_back(a[j] + x);
++j;
}
b.emplace_back(a[i]);
}
swap(a, b);
};
auto get = [&](int l, int r) {
std::vector<int> x(1);
for (int i = l; i < r; ++i) {
merge(x, a[i]);
}
return x;
};
auto la = get(0, n / 2), lb = get(n / 2, n);
int ib = 0, ans = 0;
for (int i = la.size() - 1; i >= 0; --i) {
while (ib < lb.size() && la[i] + lb[ib] > t) ++ib;
if (ib != lb.size()) ans = std::max(ans, la[i] + lb[ib]);
}
return ans;
}

int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout << solve() << std::endl;;
return 0;
}
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
// dfs 剪枝,下述算法会被制造的数据吃掉,例如 t 为奇数,所有其它数为偶数,并且 t 很大,有特别多的解。
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int solve() {
int n, t;
std::cin >> n >> t;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
std::sort(a.rbegin(), a.rend());
std::vector<LL> b(n + 1);
for (int i = n; i > 0; --i) b[i - 1] = a[i - 1] + b[i];
int ans = 0;
std::function<void(int, int)> dfs = [&](int now, int i) {
if (i > n || ans == t || ans - now >= b[i]) return;
ans = std::max(ans, now);
if (now + a[i] <= t) dfs(now + a[i], i + 1);
dfs(now, i + 1);
};
dfs(0, 0);
return ans;
}

int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout << solve() << std::endl;;
return 0;
}

1451E:交互题

题意:给定 n(为 2 的幂次,且大于 2),猜测一个长为 n ,取值在 [0, n - 1] 的数列。每次可以询问,XOR i jOR i jAND i j 中的一种($i \neq j$)。询问次数不超过 n + 1。

上面 ORAnd 用一个即可,我们这里用 AndOR 也类似。

做法:所有的数和第一个数异或(自己跟自己异或为 0,省一次查询),如果有相同的结果,那么做一个 AND 就知道第一个数为多少了,否则所有值都出现了,那么我们可以找到一个 i 使得 $r[1] \wedge r[i] = 2^n - 1$,此时 $r[1] \And r[i] = 0$,我们再找一个数 j,求 r[i] & r[j] 以及 r[1] & r[j] 再利用 a + b = a ^ b + 2 (a & b) 就可以求出 r[1] 了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 1; i < n; ++i) {
std::cout << "XOR 1 " << i + 1 << std::endl;
std::cin >> a[i];
}
int x = -1;
std::vector<int> b(n, -1);
for (int i = 0; i < n; ++i) {
if (b[a[i]] == -1) b[a[i]] = i;
else {
std::cout << "AND " << b[a[i]] + 1 << " " << i + 1 << std::endl;
int tmp;
std::cin >> tmp;
x = tmp ^ a[i];
break;
}
}
if (x == -1) {
int t1, t2;
std::cout << "AND " << b[1] + 1 << " " << b[n - 1] + 1 << std::endl;
std::cin >> t1;
t1 = t1 * 2 + n - 2;
std::cout << "AND " << 1 << " " << b[1] + 1 << std::endl;
std::cin >> t2;
t2 = t2 * 2 + 1;
x = (n - 1 + t1 + t2) / 2 - t1;
}
std::cout << "! " << x;
for (int i = 1; i < n; ++i) std::cout << " " << (x ^ a[i]);
std::cout << std::endl;
return 0;
}

其实本题另一种处理技巧,不需要 $n$ 为 2 的幂次,只需考虑异或为 1 和 2 的 i,j。下面做法基于此想法

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 1; i < n; ++i) {
std::cout << "XOR 1 " << i + 1 << std::endl;
std::cin >> a[i];
}
int x = -1;
std::vector<int> b(n, -1);
for (int i = 0; i < n; ++i) {
if (b[a[i]] == -1) b[a[i]] = i;
else {
std::cout << "AND " << b[a[i]] + 1 << " " << i + 1 << std::endl;
int tmp;
std::cin >> tmp;
x = tmp ^ a[i];
break;
}
}
if (x == -1) {
int t1, t2;
std::cout << "AND " << 1 << " " << b[1] + 1 << std::endl;
std::cin >> t1;
std::cout << "AND " << 1 << " " << b[2] + 1 << std::endl;
std::cin >> t2;
x = t1 | (t2 & 1);
}
std::cout << "! " << x;
for (int i = 1; i < n; ++i) std::cout << " " << (x ^ a[i]);
std::cout << std::endl;
return 0;
}

1451D:很好博弈问题

题意:从 (0, 0) 开始,每次可以向上或向右走 k 个单位(但是和起点距离不能超过 d),两人轮流走,谁不能走了谁输。

显然可以简化成走一个单位,距离不超过 $\frac{d}{k}$. 没过一小会,我就想到了,若 $2 x^2 \leq \frac{d^2}{k^2} < 2 (x + 1)^2$,那么,如果 $x^2 + (x + 1)^2 \leq \frac{d^2}{k^2}$,那么先手赢,否则先手输。

注意到 $x^2 + (x + 2)^2 = 2 (x + 1)^2 + 2 > d$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
LL d, k;
std::cin >> d >> k;
d = d * d / k / k;
LL x = std::sqrt(d / 2 + 0.1);
std::cout << (x * x + (x + 1) * (x + 1) <= d ? "Ashish\n" : "Utkarsh\n");
}
return 0;
}

1451C

题意:给定长为 n 的由小写字母组成的字符串 a, b,可以将 a 相邻的位置互换(因此所有位置都可以互换),也可以将长为 $k$ 且每一位都相同的字母全部变成下一个字母。

做法:一开始想排序之和贪心(这份代码),后来发现 n = 3, k = 2aab 变成 zzy 就会出问题。我还以为思路没问题,又交了一遍,属实天真。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

bool solve() {
int n, k;
std::string a, b;
std::cin >> n >> k >> a >> b;
std::sort(a.begin(), a.end());
std::sort(b.begin(), b.end());
// for (int i = 0; i < n; ++i) if (a[i] > b[i]) return 0;
int sa = 0, sb = 0, t = 0;
auto deal = [&](char c) {
for (int i = 1; i <= k; ++i) a[sa - i] = c;
sa -= k;
--t;
};
while (sb < n) {
if (sa == n) deal(b[sb]);
if (a[sa] == b[sb]) {
++sa;
++sb;
} else if (a[sa] > b[sb]) {
if (t == 0) return 0;
deal(b[sb]);
} else {
if (sa + k - 1 < n && a[sa] == a[sa + k - 1]) {
++t;
sa += k;
} else return 0;
}
}
return 1;
}

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
std::cout << (solve() ? "Yes\n" : "No\n");
}
return 0;
}

此题考虑每个数的个数会更简单!怪不得他们做的这么快…

1452E:特别难的复杂度降低问题,经典问题,注意转化

题意,有两个出题人各自连续讲 k 题,每个参赛者选择去听其中一个出题人讲题,参赛者感兴趣的题在一个区间 [l, r] 问每个参赛者能听的自己感兴趣的题目的总和最大值为多少,$n$:总题目数,$m$ 参赛人数。

首先这里有一个自然的 $O(n^2 m)$ 的做法,有些人利用 Codeforces 上支持的 GCC 指令过了题…。
官方题解说:对于每个参赛者的感兴趣区间 [l, r],某一个出题人讲题区间 [i, i + k - 1],当 i 在递增时,这两个区间的交会怎样变化呢?在脑子里把区间进行平移会发现,它先增,在它们的中间相交之后递减,并且还是对称的。也就是说,两个出题人它们区间的中点和参赛者近的会被参赛者选择。因此排序之后,第一个出题人拿前缀,后一个拿后缀。所以就是求前缀和和后缀和之和的最大值。总复杂度 $O(mn + m \log m)$

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, k;
std::cin >> n >> m >> k;
std::vector<pii> a(m);
for (auto &[l, r] : a) std::cin >> l >> r;
std::sort(a.begin(), a.end(), [](const pii &A, const pii &B) {
return A.first + A.second < B.first + B.second;
});
std::vector<int> sm(m + 1);
for (int i = 0; i + k <= n; ++i) {
int cur = 0;
for (int j = m - 1; j >= 0; --j) {
cur += std::max(0, std::min(i + k, a[j].second) - std::max(i, a[j].first - 1));
sm[j] = std::max(sm[j], cur);
}
}
int ans = sm[0];
for (int i = 0; i + k <= n; ++i) {
int cur = 0;
for (int j = 0; j < m; ++j) {
cur += std::max(0, std::min(i + k, a[j].second) - std::max(i, a[j].first - 1));
ans = std::max(ans, cur + sm[j + 1]);
}
}
std::cout << ans << std::endl;
return 0;
}

有人写了不排序的 $O(n m)$ 做法,反正我时没懂。

1452F:经典观察贪心问题

题意:有 $a_i$ 个 $2^i$ 的数,每次可以把某个 $2^{i + 1}$ 分成两个 $2^i$,给定 $x,y$,问至少需要多少次操作可以有至少 $y$ 个数小于 $2^x$。

首先,如果一个数 $2 \leq 2^l \leq 2^x$,那么分一次多一个答案,这种称作小的,可以将 $2^l$ 分成 $2^{l - x}$ 个 $2^x$,需要的次数为 $2^{l - x} + 1$。首先注意到 $l > k$ 时,分小的性价比较高,因此我们递增的考虑 $2^l > 2^x$

  • 若 $2^{l - x} \leq k$,全部搞一下就可以了
  • 否则,若 $2^{l - x} > k$,这个时候我们可以看那些小的个数是否大于等于 $k$,如果是,那结束了,否则将 $2^l$ 分割成 两个 $2^{l - 1}$,再判断,若 $2^{l - 1 - x} > k$,那么最多只会用到一个 $2^{l - 1}$,否则有一个 $2^{l - 1}$ 必然全部都拿来用了。那好了再次回到了这个判断上来了。经典!

一开始以为时线段树问题,后来一看 $n < 30$,有一堆的观察之后才好下手的!每一步的观察都要准确!才能一步一步的走向正确的答案

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, q;
std::cin >> n >> q;
std::vector<LL> a(n);
for (auto &x : a) std::cin >> x;
auto getans = [&](int x, LL y) -> LL {
LL small = 0, ans = 0;
for (int i = 0; i <= x; ++i) {
y -= a[i];
small += (a[i] << i) - a[i];
}
if (y <= 0) return 0;
auto add = [&](int i, LL t) {
ans += (t << i - x) - t;
y -= t << i - x;
small += (t << i) - (t << i - x);
};
int id = x + 1;
while (id < n) {
LL t = std::min(y >> id - x, a[id]);
if (t > 0) add(id, t);
if (t < a[id]) break;
++id;
}
if (id == n) return y > small ? -1 : y + ans;
while (y > small && id > x) {
--id;
++ans;
if (y >> id - x) add(id, 1LL);
if (id == x && y > 0) add(id, 1LL);
}
return y + ans;
};
while (q--) {
int op, x;
LL y;
std::cin >> op >> x >> y;
if (op == 1) a[x] = y;
else std::cout << getans(x, y) << std::endl;
}
return 0;
}

1440C:模拟题

题意:给定 $n \times m$ 的 0-1 矩阵,每次操作改变 $2 \times 2$ 小方块中三个位置,要求在 $nm$ 步内使得所有方块为 0。首先显然每一行可以通过 $m$ 次操作置 0,每一列同理,所以最后转化成对 $2 \times 2$ 四次内变成全 0

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, m;
std::cin >> n >> m;
std::vector<std::string> s(n);
for (auto &x : s) std::cin >> x;
std::vector<std::pair<int, int>> r;
auto f = [](char &x) {x = (x == '0' ? '1' : '0');};
for (int i = n - 1; i > 1; --i) {
for (int j = m - 1; j >= 0; --j) if (s[i][j] == '1') {
r.emplace_back(i, j);
r.emplace_back(i - 1, j);
f(s[i - 1][j]);
if (j > 0) {
r.emplace_back(i - 1, j - 1);
f(s[i - 1][j - 1]);
} else {
r.emplace_back(i - 1, j + 1);
f(s[i - 1][j + 1]);
}

}
}
for (int j = m - 1; j > 1; --j) {
for (int i = 0; i < 2; ++i) {
if (s[i][j] == '1') {
r.emplace_back(i, j);
r.emplace_back(i, j - 1);
r.emplace_back(1 - i, j - 1);
f(s[i][j - 1]);
f(s[1 - i][j - 1]);
}
}
}
std::deque<std::pair<int, int>> t[2];
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
t[s[i][j] - '0'].emplace_back(i, j);
}
}
auto g = [&](int i) {
r.emplace_back(t[i].back());
t[1 - i].emplace_front(t[i].back());
t[i].pop_back();
};
if (t[1].size() == 4) {
for (int i = 0; i < 3; ++i) g(1);
}
if (t[1].size() == 1) {
g(1);
for (int i = 0; i < 2; ++i) g(0);
}
if (t[1].size() == 2) {
g(1);
for (int i = 0; i < 2; ++i) g(0);
}
if (t[1].size() == 3) {
for (int i = 0; i < 3; ++i) g(1);
}
std::cout << r.size() / 3 << std::endl;
for (int i = 0; i * 3 < r.size(); ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << r[i * 3 + j].first + 1 << " " << r[i * 3 + j].second + 1 << " \n"[j == 2];
}
}
}
return 0;
}

1440D:图论乱搞

题意:问一个图中是否存在 k-阶完全子图,或一个度数全大于 k 的子图。

显然,我们可以把度数小于 $k - 1$ 的节点全部踢了。即从度数从小到大的遍历,小于 $k - 1$ 踢了,等于 $k - 1$, 看这 $k$ 个点能否成为完全图,能?结束,否则,继续,直到当前度数为 $k$,或者所有点都被剔除。

没能在比赛的时候做出原因:

  • 没有注意到删点后更新度数的次数只和边数有关,所以当时觉得复杂度过不了!
  • 在完全图判断时不够自信(单次复杂度 $k^2$,但是注意到边数小于 $\frac{(k - 1)k}{2}$ 时不可能为完全图)。
  • 总复杂度 $O(m \sqrt{m} \log n)$,下面代码 998ms 飘过(在死亡的边缘疯狂试探)
  • unordered_set 是基于 hash 表的,如果不需要集合按顺序输出,可以作为优先选择。
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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

void solve() {
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::unordered_set<int>> e(n);
for (int i = 0, x, y; i < m; ++i) {
std::cin >> x >> y;
--x; --y;
e[x].insert(y);
e[y].insert(x);
}
std::set<std::pair<int, int>> d;
for (int i = 0; i < n; ++i) d.insert({e[i].size(), i});
auto check = [&](int u) -> bool {
std::vector<int> tmp(e[u].begin(), e[u].end());
for (int i = 0; i < tmp.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (e[tmp[i]].count(tmp[j]) == 0) return false;
}
}
return true;
};
auto del = [&](int u) {
for (auto v : e[u]) {
d.erase({e[v].size(), v});
e[v].erase(u);
d.insert({e[v].size(), v});
--m;
}
};
while (!d.empty()) {
int du = d.begin()->first;
int u = d.begin()->second;
if (du >= k) {
std::cout << 1 << " " << d.size() << "\n";
for (auto it = d.begin(); it != d.end(); ++it) {
std::cout << (it->second + 1) << " ";
}
std::cout << "\n";
return;
}
if (du == k - 1 && k - 1 <= m * 2 / k && check(u)) {
std::cout << 2 << "\n";
std::cout << u + 1 << " ";
for (auto x : e[u]) std::cout << x + 1 << " ";
std::cout << "\n";
return;
}
d.erase(d.begin());
del(u);
}
std::cout << "-1\n";
}

int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
solve();
}
return 0;
}

1440E: 线段树

没能在比赛的时候做出原因:

  • 线段树模板不太好用
  • 没有注意到数列必然单调递减,所以处理的时候处理复杂了
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
96
97
98
99
100
101
102
103
104
105
106
107
108
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

struct SegmentTree {
int n;
std::vector<int> mn, tag;
std::vector<LL> sm;
#define lson l, m, 2 * p
#define rson m + 1, r, 2 * p + 1
void resize() {
mn.resize(4 * n);
tag.resize(4 * n);
sm.resize(4 * n);
}
SegmentTree(int _n) : n(_n) {
resize();
}
SegmentTree(const std::vector<int> &a) {
n = a.size();
resize();
std::function<void(int, int, int)> build = [&](int l, int r, int p) {
if (l == r) {
mn[p] = sm[p] = a[l - 1];
return;
}
int m = (l + r) / 2;
build(lson);
build(rson);
pull(p);
};
build(1, n, 1);
}
void pull(int p) {
mn[p] = std::min(mn[2 * p], mn[2 * p + 1]);
sm[p] = sm[2 * p] + sm[2 * p + 1];
}
void set(int l, int r, int p, int v) {
tag[p] = mn[p] = v;
sm[p] = LL(r - l + 1) * v;
}
void push(int l, int r, int p) {
if (tag[p]) {
int m = (l + r) / 2;
set(lson, tag[p]);
set(rson, tag[p]);
tag[p] = 0;
}
}
void rangeSet(int L, int R, int v, int l, int r, int p) {
if (L <= l && R >= r) {
set(l, r, p, v);
return;
}
int m = (l + r) / 2;
push(l, r, p);
if (L <= m) rangeSet(L, R, v, lson);
if (R > m) rangeSet(L, R, v, rson);
pull(p);
}
int query(int x, int& y, int l, int r, int p) {
if (mn[p] > y) return 0;
if (x <= l && sm[p] <= y) {
y -= sm[p];
return r - l + 1;
}
int m = (l + r) / 2;
push(l, r, p);
int ans = 0;
if (x <= m) ans += query(x, y, lson);
ans += query(x, y, rson);
return ans;
}
int query(int x, int y) {
return query(x, y, 1, n, 1);
}
int bounded(int v, int l, int r, int p) {
if (mn[p] >= v) return r + 1;
if (l == r) return l;
int m = (l + r) / 2;
if (mn[2 * p] >= v) return bounded(v, rson);
return bounded(v, lson);
}
void modify(int x, int y) {
int l = bounded(y, 1, n, 1);
if (l <= x) rangeSet(l, x, y, 1, n, 1);
}
};

int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, q;
std::cin >> n >> q;
std::vector<int> a(n);
for (auto &x: a) std::cin >> x;
SegmentTree A(a);
while (q--) {
int op, x, y;
std::cin >> op >> x >> y;
if (op == 1) A.modify(x, y);
else std::cout << A.query(x, y) << std::endl;
}
return 0;
}

1447B:简单细节题

  • 很早就知道,任意两个可以换,要考虑非正数的个数是否为奇数。
  • 然后为奇数时,绝对值总和减去 2 倍的最大的非正数(!!!这是错了)。
  • 后来想了半天终于知道时减去绝对值最小的数。
  • 然后再之前的内容上改,写出了下面屎一样的 RE 代码!还 PE 了,最后 RE,吐了!

此代码为 RE 代码!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n, m;
std::cin >> n >> m;
n *= m;
std::vector<int> a, b;
int r = 0;
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
r += abs(x);
if (x <= 0) a.emplace_back(-x);
else b.emplace_back(x);
}
if (a.size() & 1) {
r -= std::min(*std::min_element(a.begin(), a.end()), *std::min_element(b.begin(), b.end())) * 2;
}
std::cout << r << std::endl;
}
return 0;
}

1447C:经典问题:假 0-1 背包,真贪心

题意:给定 $n$ 件物品 $w_i$,给出一个 $k$ 件物品和在 $[\lfloor \frac{W}{2} \rfloor, W]$ 之间的一种方案

做法:对物品从大到小排序,然后贪心即可。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
LL w;
std::cin >> n >> w;
std::vector<std::pair<int, int>> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i].first;
a[i].second = i + 1;
}
std::sort(a.begin(), a.end(), std::greater<>());
std::vector<int> x;
bool flag = false;
LL s = 0, h = (w + 1) / 2;
for (int i = 0; i < n; ++i) {
if (s + a[i].first <= w) {
x.emplace_back(a[i].second);
s += a[i].first;
if (s >= h) {
flag = true;
break;
}
}
}
if (flag) {
std::sort(x.begin(), x.end());
std::cout << x.size() << std::endl;
for (auto i : x) std::cout << i << " ";
std::cout << std::endl;
} else std::cout << -1 << std::endl;
}
return 0;
}

1447D:DP

就是一个简单的 DP,写完我都有点虚…

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::string a, b;
std::cin >> n >> m >> a >> b;
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1));
int r = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
dp[i][j] = std::max({0, dp[i - 1][j] - 1, dp[i][j - 1] - 1});
if (a[i - 1] == b[j - 1]) dp[i][j] = std::max(dp[i][j], dp[i - 1][j - 1] + 2);
r = std::max(r, dp[i][j]);
}
}
std::cout << r << std::endl;
return 0;
}

144E:思维题

  • 考虑最高位,如果最高位为 1,最高位为 0 的个数都大于 1,那么它们必然不连通,所以我们要将其中的一个变得不超过 1,注意不能贪心。并且注意到如果元素个数不超过 2,必然连通。
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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
std::function<int(std::vector<int>, int)> dfs = [&](std::vector<int> a, int now) -> int {
if (now < 0 || a.size() <= 2) return 0;
std::vector<int> b, c;
for (auto x : a) {
if ((x >> now) & 1) b.emplace_back(x);
else c.emplace_back(x);
}
return std::min(dfs(b, now - 1) + std::max(0, int(c.size() - 1)), dfs(c, now - 1) + std::max(0, int(b.size() - 1)));
};
std::cout << dfs(a, 29) << std::endl;
return 0;
}

AtCoder ABC 183f:并查集 + map

题意:N 个节点,每个节点有一个值,然后 Q 次操作:1 a b 是将 a, b 所在的群合并,2 x y 求 $x$ 所在的群中,值为 $y$ 的个数。细节优化

  • 尽量小的向大的合并,合并完小的记得清空
  • std::map 优于 std::multiset
  • std::vector<std::map<int, int>> 优于 std::map<std::map<int, int>>
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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, q;
std::cin >> n >> q;
std::vector<int> c(n), p(n);
std::iota(p.begin(), p.end(), 0);
std::vector<std::map<int, int>> mp(n);
for (auto &x : c) std::cin >> x, --x;
for (int i = 0; i < n; ++i) ++mp[i][c[i]];
std::function<int(int)> find = [&](int x) -> int {
int ans = x;
while (ans != p[ans]) ans = p[ans];
while (x != ans) {
int t = p[x];
p[x] = ans;
x = t;
}
return ans;
};
auto father = [&](int i, int pi) {
p[i] = pi;
for (auto it = mp[i].begin(); it != mp[i].end(); ++it) {
mp[pi][it->first] += it->second;
}
mp[i].clear();
};
auto merge = [&](int i, int j) {
int fi = find(i), fj = find(j);
if (fi != fj) {
if (mp[fi].size() < mp[fj].size()) father(fi, fj);
else father(fj, fi);
}
};
while (q--) {
int op, a, b;
std::cin >> op >> a >> b;
--a; --b;
if (op == 1) {
merge(a, b);
} else {
int fa = find(a);
std::cout << mp[fa][b] << std::endl;
}
}
return 0;
}

AtCoder ABC 183e:经典 DP

题意:在 $n \times m$ 的格点中,有些点可以走有些不行,每次能往右,下或右下中的一个方向走任意步(但是中间不能有非法点),问有多少种从左上角到右下角的走法。

显然 DP,然后用类和优化

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

const int M = 1e9 + 7;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> a(n, std::vector<int>(m)), al(n, std::vector<int>(m)), au(n, std::vector<int>(m)), ad(n, std::vector<int>(m));
a[0][0] = 1;
std::vector<std::string> s(n);
for (auto &x : s) std::cin >> x;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (s[i][j] == '.') {
if (j > 0) (a[i][j] += al[i][j - 1]) %= M;
if (i > 0) (a[i][j] += au[i - 1][j]) %= M;
if (i > 0 && j > 0) (a[i][j] += ad[i - 1][j - 1]) %= M;
al[i][j] = au[i][j] = ad[i][j] = a[i][j];
if (j > 0) (al[i][j] += al[i][j - 1]) %= M;
if (i > 0) (au[i][j] += au[i - 1][j]) %= M;
if (i > 0 && j > 0) (ad[i][j] += ad[i - 1][j - 1]) %= M;
} else {
a[i][j] = al[i][j] = au[i][j] = ad[i][j] = 0;
}
}
}
std::cout << a[n - 1][m - 1] << std::endl;
return 0;
}

1438 数学场

A 题:$a_i = 1$ 即可
B 题:若存在 $i \neq j$ 使得 $a_i = a_j$,则 YES,否则 NO(考虑二进制)
C 题:可以根据假设强制让 $a_{i + j}$ 与 $i + j$ 有相同的奇偶性,那么必然满足条件,此问题一般化 解法
D 题:注意到首先我们可以让数列成对相等并且 $x \otimes x \otimes y = y$,所以如果 $n$ 为奇数,必然就 YES,若 $n$ 为偶数,我们成对这样搞之后,会有两个元素可能不等,又注意到它们相等当且仅当所有值异或为 0,所以搞定

代码索然无味就不写了

Atcoder ABC182F:找钱问题

给定 $n$ 种纸币,$1 = a_1 < a_2 < \cdots a_n$,且 $a_i | a_{i + 1}$,要买商品 $x$,那么可以给 $y \geq x$,找零 $y - x$,要求 $y$ 和 $y - x$ 的最少纸币表达中没有公共纸币。问所有的 $y$ 有没有种。

注意到 $y$ 和 $(y_1, \cdots, y_n)$,(其中 $y_i * a_i < a_{i + 1}$,且 $y = \sum a_i y_i$ 有一个一一对应。然后我们可以考虑 $x$ 的向量表达,然后再看 $y + x$ 和 $y$ 没公共非零项的做法。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
LL x;
std::cin >> n >> x;
std::vector<LL> a(n);
for (auto &x : a) std::cin >> x;
x %= a.back();
std::map<LL, LL> mp;
mp.insert({x, 1});
for (int i = 1; i < n; ++i) {
std::map<LL, LL> mp2;
for (auto it = mp.begin(); it != mp.end(); ++it) {
mp2[it->first / a[i]] += it -> second;
if (it->first % a[i]) mp2[it->first / a[i] + 1] += it->second;
}
for (int j = i + 1; j < n; ++j) a[j] /= a[i];
std::swap(mp, mp2); // 用 swap 更快!
}
std::cout << mp[0] + mp[1] << std::endl;
return 0;
}

凸优化借助凸包解决

详细解释放在 izlyforever

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
using pdd = std::pair<double, double>;

const double eps = 1e-12;
int dcmp(double x) {
return fabs(x) < eps ? 0 : (x > 0 ? 1 : -1);
}
bool crossLeft(const pii &op, const pii &sp, const pii &ep) {
return (sp.first - op.first) * (ep.second - op.second)
< (sp.second - op.second) * (ep.first - op.first);
}
std::vector<pdd> convexHull(std::vector<pdd> p) {
std::sort(p.begin(), p.end());
p.erase(std::unique(p.begin(), p.end()), p.end());
int n = p.size();
std::vector<pdd> q(n + 1);
int top = 0;
for (int i = 0; i < n; ++i) {
while (top > 1 && crossLeft(q[top - 1], p[i], q[top - 2])) --top;
q[top++] = p[i];
}
int len = top;
for (int i = n - 2; i >= 0; --i) {
while (top > len && crossLeft(q[top - 1], p[i], q[top - 2])) --top;
q[top++] = p[i];
}
top -= n > 1;
q.resize(top);
return q;
}
int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, d, c;
std::cin >> n >> d;
std::vector<pdd> p(n);
for (auto &[x, y] : p) {
std::cin >> c >> x >> y;
x = x * d / c;
y = y * d / c;
}
auto q = convexHull(p);
auto cal = [](pdd p) {
return p.first * p.second;
};
auto deal = [](pdd a, pdd b) -> double {
a.first -= b.first; a.second -= b.second;
if (a.first * a.second < 0) {
double t = -(b.first / a.first + b.second / a.second) / 2;
if (t > 0 && t < 1) return (a.first * t + b.first) * (a.second * t + b.second);
}
return 0;
};
double r = -1;
for (auto &x : q) r = std::max(r, cal(x));
for (int i = 0; i != q.size(); ++i) {
r = std::max(r, deal(q[i], q[(i + 1) % q.size()]));
}
std::cout.precision(12);
std::cout << std::fixed << r << std::endl;
return 0;
}

Atcoder arc107C:并查集

首先,如果某两列和交换,那么给它们连边,那么只要任意两列可达,那么它们的位置最后就可以交换,也就是求每个连通分支的大小,直接广搜标记也可以做,当然了用并查集会更简单。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
const LL M = 998244353;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<LL> fac(n + 1);
fac[0] = 1;
for (int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i % M;
std::vector<std::vector<int>> a(n, std::vector<int>(n));
for (auto &x : a) for (auto &i : x) std::cin >> i;
auto f = [&]() {
std::vector<int> p(n);
std::iota(p.begin(), p.end(), 0);
auto find = [&](int x) {
int ans = x;
while (ans != p[ans]) ans = p[ans];
while (x != ans) {
int t = p[x];
p[x] = ans;
x = t;
}
return ans;
};
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
bool flag = true;
for (int t = 0; t < n; ++t) {
if (a[i][t] + a[j][t] > k) {
flag = false;
break;
}
}
if (flag) p[find(j)] = p[find(i)];
}
}
for (int i = 0; i < n; ++i) find(i);
LL r = 1;
for (int i = 0; i < n; ++i) {
int cnt = std::count(p.begin(), p.end(), i);
r = r * fac[cnt] % M;
}
return r;
};
LL r = f();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
std::swap(a[i][j], a[j][i]);
}
}
r = r * f() % M;
std::cout << r << std::endl;
return 0;
}

Atcoder arc107D:经典计算,DP 优化

将 $K$ 写成 $N$ 个形如 $2^{-i}, i \geq 0$ 之和(不计顺序),问有多少中写法。我们不妨将答案记作 dp[n][k]
那么显然 dp[n][k] = dp[n][2k] + dp[n - 1][2k - 2] + \cdots dp[n - k][0](考虑取多少个 1,那么剩下的最少要以 $\frac{1}{2}$ 为最大值,那么就等价于剩下的数乘以 2),所以令 s[a] = dp[a][0] + dp[a + 1][2] + \cdots dp[n][2(n - a)],这样我们就能迅速求出 dp[n][k]

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
const LL M = 998244353;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(n + 1));
for (int i = 0; i <= n; ++i) dp[i][i] = 1;
std::vector<int> s(n + 1);
s[0] = 1;
auto add = [](int &x, int y) {
(x += y) >= M && (x -= M);
};
for (int i = 1; i <= n; ++i) {
for (int j = i; j > 0; --j) {
dp[i][j] = s[i - j];
if (j % 2 == 0) add(s[i - (j / 2)], dp[i][j]);
}
}
std::cout << dp[n][k] << std::endl;
return 0;
}

1442D:经典分治问题

首先这是一个很实在的问题。给定 $n$ 个单调递增的序列,从中取 $k$ 个数,但是取数的时候每次只能在序列的最前面取,也就是取最小的。求最大的和。首先注意最多只有一个序列取了一部分,其它的要么没取,要么取完(反证),那么我们可以二分枚举其在左边还是在右边

如果直接暴力写,也就是枚举那个只取了一部分的,其它的就是一个 0-1 背包,所以总复杂度为 $O(n^2 k)$,这肯定是过不了的。但是可以分治,也就是说分成两半,一半是 0-1 背包(即要么取完要么没取),另一半是原问题的子问题!这不就有了吗,经典!!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
// freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<std::vector<LL>> a(n);
for (auto &p : a) {
int cnt;
std::cin >> cnt;
p.emplace_back(0);
for (int i = 0, x; i < cnt; ++i) {
std::cin >> x;
if (i < k) p.push_back(p.back() + x);
}
}
std::vector<LL> dp(k + 1, -1e18);
dp[0] = 0;
auto merge = [&](int l, int r) {
for (int i = l; i < r; ++i) {
for (int j = k; j >= a[i].size() - 1; --j) {
dp[j] = std::max(dp[j], dp[j - a[i].size() + 1] + a[i].back());
}
}
};
LL ans = 0;
std::function<void(int, int)> divide = [&](int l, int r) {
if (l + 1 >= r) {
for (int i = 0; i < a[l].size(); ++i) ans = std::max(ans, dp[k - i] + a[l][i]);
return;
}
int m = (l + r + 1) / 2;
auto tmp = dp;
merge(l, m);
divide(m, r);

dp = tmp;
merge(m, r);
divide(l, m);
};
divide(0, n);
std::cout << ans << std::endl;
return 0;
}

本来是每一层都是一个 dp,但是 Itst 做了空间优化
知道在某一个性质的点上取得最值,那么不一定要把这个点求出来,可以在一定范围内把值都比较一遍即可,因为极值点的判断有可能相对更为复杂,这可能就是计算机的魅力吧。
另外 Jiangly 写了一个非递归的 $\sqrt{n}$ 的做法,也很犀利。

1443D:经典问题

给定一个非负数列,问是否可以通过前缀减一,后缀减一的方式使得所有的数都变成 0

一开始以为只需中间的数大于两边的最小值之和就可以,后来发现不对,然后通过自己想了一个例子,然后想到了正确做法,开心
这个问题等价与给定 $a$,求非负序列 $p, q$ 满足 $p + q = a$, $p$ 单调减,$q$ 单调递增(在保证条件下,使得$p$ 尽量大)

做法:看相邻两个数,比如左边比右边大,那么必然右边至少要做后缀减一的操作它们的差值次,也就是说,后面所有的数都要减去这个差值。反之同理,所以搞两个变量,一个是左边累减去(可以用剩余多少来标记),一个是右边累减。没跑一步判断一次。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas = 1;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
bool flag = true;
int now = a[0], cur = 0;
for (int i = 1; i < n; ++i) {
if (a[i] < cur) {
flag = false;
break;
}
if (a[i] > a[i - 1]) {
cur += a[i] - a[i - 1];
} else {
now -= a[i] - a[i - 1];
if (now < 0) {
flag = false;
break;
}
}
}
std::cout << (flag ? "YES\n" : "NO\n");
}
return 0;
}

1445D:一个 trival 的脑力问题

给定长为 $2n$ 的序列 $a$,分成两个长度为 $n$ 的序列 $p, q$ 然后 $p$ 非降,$q$ 非升,定义 $f(p, q) = \sum_{i = 1}^n |p_i - q_i|$,问所有的 $f(p, q)$ 的和为多少。

不妨设 $a$ 是有序的,平均分两半,如果 $p$ 在左边取了 $k$ 个元素,那么 $q$ 必然在右边取了 $k$ 个元素,所以无论哪种情况,$f(p, q)$ 是常数。所以结论就显然了!所以可以搞个升级版!。

答案就是 $\binom{2n}{n} \sum_{i = 1}^{n} (a_{i + n} - a_i)$,代码就不贴了。推公式把我推吐了。

此题已经被我魔改了,哈哈哈

1437C:经典问题之动态规划

给定 $1 \leq a_i \leq n$,求两两不同的正整数 $b_i$,使得 $\sum_{i = 1}^n |a_i - b_i|$ 最小。
显然只要 $b$ 的值域确定了,答案就确定了,且 $1 \leq b_i < 2n$。所以就可以对 $a_i$ 排序,然后再 1 ~ 2n - 1 中选 $n$ 个数,使得结果最小。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
std::vector<int> dp(n + 1, 1e9);
std::sort(a.begin(), a.end());
dp[0] = 0;
for (int i = 1; i < 2 * n; ++i) {
for (int j = n; j > 0; --j) dp[j] = std::min(dp[j], dp[j - 1] + abs(a[j - 1] - i));
}
print(dp[n]);
}
return 0;
}

1437E:最长递增子序列

在固定数组的一些元素的条件下,最小改变多少数,使得数列严格单调递增(减去标号就变成 不严格 递增了)

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<int> a(n + 2);
a[0] = -1e9 - 2, a[n + 1] = 1e9 + 2;
for (int i = 1; i <= n; ++i) {
std::cin >> a[i];
a[i] -= i;
}
std::vector<int> b(k + 2);
b[0] = 0, b[k + 1] = n + 1;
for (int i = 1; i <= k; ++i) std::cin >> b[i];
for (int i = 2; i <= k; ++i) if (a[b[i]] < a[b[i - 1]]){
print(-1);
return 0;
}
int r = n - k;
for (int i = 0; i <= k; ++i) {
int now = 0;
std::vector<int> c;
for (int j = b[i] + 1; j < b[i + 1]; ++j) {
if (a[j] >= a[b[i]] && a[j] <= a[b[i + 1]]) {
auto it = std::upper_bound(c.begin(), c.end(), a[j]);
if (it == c.end()) c.push_back(a[j]);
else *it = a[j];
}
}
r -= c.size();
}
print(r);
return 0;
}

1435C:经典选择问题

题意大致可以转化成:有 $n$ 个人,每个人有 $m$ 个值可以选择,问如何选择才能使得他们的最大值减最小值最小。

做法就是把所有可能的选择进行排序(二维,一维存值,一维存人),然后看所有人至少一次选择的时候最大值和最小值的差是多少。相当于左右两个指针在跑,复杂度为 $O(nm)$

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int m = 6;
std::vector<int> a(m);
for (auto &x : a) std::cin >> x;
std::sort(a.begin(), a.end());
int n;
std::cin >> n;
std::vector<int> b(n);
for (auto &x : b) std::cin >> x;
std::vector<pii> p;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
p.push_back({b[j] - a[i], j});
}
}
std::sort(p.begin(), p.end());
std::vector<int> cnt(n);
int r = 1e9 + 2;
for (int i = 0, j = 0, x = 0; i < p.size(); ++i) {
while (x < n && j < p.size()) {
if (cnt[p[j].second] == 0) ++x;
++cnt[p[j].second];
++j;
}
if (x < n) break;
if (--cnt[p[i].second] == 0) --x;
r = std::min(r, p[j - 1].first - p[i].first);
}
print(r);
return 0;
}

一开始想枚举最小值,三分法来做(但是我知道凸性一般是不成立的)

1435D:经典进出问题

将 $1 ~ n$ 个元素进行加入和提出操作(每个元素一次,共 $2n$)次,并且每次出的是当前集合中最小的值。
给定一个进出序列,和出的时候的元素值,求进的元素值(不合理的输出:NO)。

注意到每出一个元素,那么集合中剩下的元素都要大于出的元素,但是这个信息可以由顶元素上加限制来承载。当在加入一个新元素后,这个限制又被暂时的隐藏了。所以存在当前顶元素上是特别优质的做法。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
int now = 0;
std::vector<int> r(n);
std::stack<pii> A;
for (int i = 0, x; i < 2 * n; ++i) {
char op;
std::cin >> op;
if (op == '+') A.push({now++, 0});
else {
std::cin >> x;
if (A.empty() || A.top().second > x) {
print("NO");
return 0;
} else {
r[A.top().first] = x;
A.pop();
if (!A.empty()) A.top().second = std::max(A.top().second, x);
}
}
}
print("YES");
for (auto x : r) std::cout << x << " ";
println;
return 0;
}

此题一开始题意理解有点问题,最后竟然还 PA,结果最终评测 WA 了。

AtCoder arc106d:经典求和计算

对任意 $1 \leq x \leq K$ 求 $\sum_{1 \leq i < j \leq n} (a_i + a_j)^x$

注意到

然后二项式展开即可。

若此题 $n$ 比较小,$k$ 比较大($n$ 特别小时直接 $n^2 \log k$ 就没啥意思了),注意到二项式展开之后是个卷积形式,所以用 NTT 有 $O(nk + k \log k)$ 的做法。例如 $n < 3 \cdot 10^4, k < 10^5$ (时限 5s)

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;
const LL M = 998244353;
const LL inv2 = (M + 1) / 2;
LL powMod(LL x, LL n) {
LL r(1);
while (n) {
if (n & 1) r = r * x % M;
n >>= 1;
x = x * x % M;
}
return r;
}
int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<LL> a(n);
for (auto &x : a) std::cin >> x;
std::vector<LL> fac(k + 1), ifac(k + 1);
fac[0] = ifac[0] = 1;
for (int i = 1; i <= k; ++i) fac[i] = fac[i - 1] * i % M;
ifac[k] = powMod(fac[k], M - 2);
for (int i = k; i > 0; --i) ifac[i - 1] = ifac[i] * i % M;
std::vector<LL> s(k + 1);
for (int i = 0; i < n; ++i) {
LL p = 1;
for (int j = 0; j <= k; ++j) {
s[j] += p;
if (s[j] >= M) s[j] -= M;
p = p * a[i] % M;
}
}
auto C = [&](int n, int m) {
return fac[n] * ifac[m] % M * ifac[n - m] % M;
};
// here nft can be used
for (int i = 1; i <= k; ++i) {
int r = 0;
for (int j = 0; j <= i; ++j) {
r = (r + C(i, j) * s[j] % M * s[i - j] % M) % M;
}
r = (r - powMod(2, i) * s[i]) % M;
if (r < 0) r += M;
r = r * inv2 % M;
print(r);
}
return 0;
}

1436E: MEX

求 $MEX(MEX(L, R)_{1 \leq L \leq R \leq n}$),其中 $MEX(L, R)$ 为使得 $a_L, a_{L + 1} \cdots, a_{R}$ 中没出现的最小正整数。

做法:先求出所有 MEX(i, n),这是能在 $O(n)$ 时间复杂度解决的(因为 MEX 会随着 i 递减而增大,并且值域不超过 $n$)。然后我们删除尾部的点,那么在从右往左首次出现 $Mex(i, n) > a[n]$ 的 pre[n] + 1 ~ i 这一段的值都要改成 a[n],这里 pre[x] 表示 x 位置前一个值为 a[x] 的位置。那么区间线段树就搞定了。

没在比赛时候写出,不想写了!

hdu 4747: MEX

求 $\sum_{1 \leq L \leq R \leq n} MEX(L, R)$),其中 $MEX(L, R)$ 为使得 $a_L, a_{L + 1} \cdots, a_{R}$ 中没出现的最小自然数

同理与上面做法,代码不写了,懒得写线段树。

有的 MEX 定义包含 0, 有的不包含,无所谓啦。

1433G: 将某条边的权值置零下最短路径

我一开始以为是缩点… 想太多了,做法就是先求出任意两点的距离,然后边 x -> y 置零可以看成 a -> b 多了两种选择 a -> x - > y - > ba -> y -> x -> b 就好了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
auto cmax = [](auto &x, auto y) {
if (x < y) x = y;
};
auto cmin = [](auto &x, auto y) {
if (x > y) x = y;
};
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::vector<int>> d(n, std::vector<int>(n, 1e9));
for (int i = 0; i < n; ++i) d[i][i] = 0;
std::vector<std::tuple<int, int, int>> road(m);
for (auto &[x, y, w] : road) {
std::cin >> x >> y >> w;
--x; --y;
cmin(d[x][y], w);
d[y][x] = d[x][y];
}
std::vector<std::pair<int, int>> travel(k);
for (auto &[a, b] : travel) {
std::cin >> a >> b;
--a; --b;
}
auto floyd = [&](){
for(int k = 0; k != n; ++k)
for(int i = 0; i != n; ++i)
for(int j = 0; j != n; ++j)
cmin(d[i][j], d[i][k] + d[k][j]);
};
floyd();
LL r = 1e9;
for (auto [x, y, w] : road) {
LL now = 0;
for (auto [a, b] : travel) {
now += std::min({d[a][b], d[a][x] + d[y][b], d[a][y] + d[x][b]});
}
cmin(r, now);
}
print(r);
return 0;
}

用堆优化 Dijkstra 会更快一些,用 priority_quque 比 set 快一些。不过 set 比 priority_quque 方便很多(好遍历删除等操作)。

439D:三分法模板题

每次操作可以增加某个数或者减少某个数,问最小多少次操作可以让 a 的最小值不小于 b 的最大值

我们可以枚举 a 最终的最小值 t,那么答案就是 $\sum_{a_i < t} (t - a_i) + \sum_{b_i > t} (b_i - t)$
求导然后就知道了它是凸函数了。然后就可以三分法求了。

三分法求凸函数最值

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> a(n), b(m);
for (auto &x : a) std::cin >> x;
for (auto &x : b) std::cin >> x;

auto f = [&](int ma) {
LL r = 0;
for (auto x : a) if (x < ma) r += ma - x;
for (auto x : b) if (x > ma) r += x - ma;
return r;
};

int l = 1, r = 1e9;
while (l + 2 < r) {
int lm = (2ll * l + r) / 3, rm = (l + 2ll * r + 2) / 3;
if (f(lm) < f(rm)) r = rm;
else l = lm;
}
while (l < r) {
if (f(l) < f(r)) --r;
else ++l;
}

print(f(l));

return 0;
}

当然了此题有更简单的做法:将 a 升序,b 降序,然后答案就是 a[i] - b[i] 为正的和。

1421A:异或,和,交的关系

$(a \oplus x) + (x \oplus b)$ 的最小值,$a \oplus b$,此时 x = a & b

1421E:观察总结题(加减号问题)

  • 任何情况下的答案都是 每一项前添加正负号得到的。
  • 相邻两个之间添加的正负号必然有一个是相同的,即不会出现 +-+-+-+-+- 的情况
  • 负号个数 $m$,总个数 $n$,满足 $(n + m) \equiv 1 \mod 3$(可以通过归纳得到,并且 $n$ 个正负号的情况可以看作 $n - 1$ 个正负号中某一个改变符号并且出现两个,再塞进去)
  • 所有的情况如上所言(可以数学归纳证明)

我们用 DP[n][3][2][2] 保存全部状态。DP[i][j][k][p] 分别表示当前 i 个位置,(i + 负数个数 mod 3) 等于 jk 表示是否非法(是否存在连续的正或负),p 表示最后以为是否为正的最大结果。

例如 DP[i][j][1][1] 表示前 i 位(i 与 前 i 位负号个数的和模 3 位 j) 且有连续的负号或者正号,且第 i 位为负号的最大和。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

const int N = 2e5 + 2;
LL dp[N][3][2][2];

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::memset(dp, -0x3f, sizeof(dp));
auto upmax = [](LL &a, LL b) {
if (a < b) a = b;
};
int n;
std::cin >> n;
std::vector<int> a(n);
for (auto &x : a) std::cin >> x;
if (n == 1) {
print(a[0]);
return 0;
}
dp[0][1][0][0] = a[0];
dp[0][2][0][1] = -a[0];
for (int i = 1; i < n; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 2; ++k) {
for (int p = 0; p < 2; ++p) {
for (int b = 0; b < 2; ++b) {
int digit = (j + 1 + b) % 3;
upmax(dp[i][digit][k | (p == b)][b], dp[i - 1][j][k][p] + (b == 0 ? a[i]: -a[i]));
}
}
}
}
}
print(std::max(dp[n - 1][1][1][0], dp[n - 1][1][1][1]));
return 0;
}

此题多维数组写 Vector 不方便,但是又不能开局部数组(因为太大了),所以只能全局变量啦。

1428D:模拟题

由简单到复杂一步步的来,很不错的题。

1428E:经典问题,正整数划分最值问题(可看作优先队列模板题)

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
using pii = std::pair<int, int>;
using pll = std::pair<LL, LL>;

LL val(const int &x, const int &n) {
int xn = x / n, rn = x - xn * n;
return 1ll * xn * xn * (n - rn) + 1ll * (xn + 1) * (xn + 1) * rn;
}
LL cmpVal(const pii &A) {
return val(A.first, A.second) - val(A.first, A.second + 1);
}
class cmp {
public:
bool operator() (const pii &lhs, const pii &rhs) const {
return cmpVal(lhs) < cmpVal(rhs);
}
};

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::priority_queue<pii, std::vector<pii>, cmp> Q;
for (int i = 0, x; i < n; ++i) {
std::cin >> x;
Q.push({x, 1});
}
while (n < k) {
++n;
auto [x, cnt] = Q.top();
Q.pop();
Q.push({x, cnt + 1});
}
LL r = 0;
while (!Q.empty()) {
auto [x, cnt] = Q.top();
Q.pop();
r += val(x, cnt);
}
print(r);
return 0;
}

若此题 $k$ 特别大,可二分 cmpVal 的题来解决。

1428F: 经典 DP

题意:给定 0-1 序列 S,设 $f(l, r)$ 表示 $S_l S_{l + 1} \cdots, S_{r}$ 中最长连续 1 的个数,求 $\sum_{l = 1}^n \sum_{r = l}^n f(l, r)$

官方题解 实在是太精彩了!这种直方图的做法真的很 Nice!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::string s;
std::cin >> n >> s;
LL ans = 0, cur = 0;
std::vector<int> left(n + 1, -1);
for (int i = 0; i < n; ++i) {
if (s[i] == '0') ans += cur;
else {
int l = i, r = i;
while (r < n && s[r] == '1') ++r;
int len = 0;
for (i = l; i < r; ++i) {
++len;
cur += i - left[len];
ans += cur;
left[len] = r - len;
}
--i;
}
}
print(ans);
return 0;
}

1430F:反向 DP

我想到反向 DP (DP[i] 表示这次本次要预留的子弹数为多少),但是还是不明确怎么写。我们可以看每一波的时候至少需要多少子弹预留。然后合理性检测,最后再从头到尾走一波就知道用多少子弹。把检测和计算分两次搞。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
std::cin >> n >> k;
std::vector<int> a(n), l(n), r(n), dp(n);
for (int i = 0; i < n; ++i) std::cin >> l[i] >> r[i] >> a[i];
for (int i = n - 1; i >= 0; --i) {
int need = a[i];
if (i < n - 1 && r[i] == l[i + 1]) need += dp[i + 1];
if (LL(r[i] - l[i] + 1) * k < need) {
print(-1);
return 0;
}
dp[i] = std::max(0LL, need - LL(r[i] - l[i]) * k);
}
int cur = k;
LL ans = 0;
for (int i = 0; i < n; ++i) {
ans += a[i];
if (cur < dp[i]) {
ans += cur;
cur = k;
}
cur = ((cur - a[i]) % k + k) % k;
}
print(ans);
return 0;
}

1430E:可以转化成求逆序数

这种只能交换相邻位置的问题,一般都能转换成逆序数。我们先根据最后的状态来定义序关系,然后对应赋值,再求逆序数就好了!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

struct TreeArray {
std::vector<LL> s;
TreeArray() {}
TreeArray(int n) { init(n); }
void init(int n) {
s.resize(n + 1);
std::fill(s.begin(), s.end(), 0);
}
int lowbit(int n) {
return n & (-n);
}
void add(int id, int p) {
while (id < s.size()) {
s[id] += p;
id += lowbit(id);
}
}
LL sum(int id) {
LL r = 0;
while (id) {
r += s[id];
id -= lowbit(id);
}
return r;
}
// find minimal index s.t. sum(id) >= x, sum must be increased
int search(LL val) {
LL sum = 0;
int id = 0;
for (int i = std::__lg(s.size()); ~i; --i) {
if (id + (1 << i) < s.size() && sum + s[id + (1 << i)] < val) {
id += (1 << i);
sum += s[id];
}
}
return ++id;
}
};

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::string s;
std::cin >> n >> s;
std::vector<int> tmp[26];
int now = 0;
for (int i = s.size() - 1; i >= 0; --i) tmp[s[i] - 'a'].emplace_back(++now);
std::vector<int> a(n + 1), p(n + 1);
for (int i = n; i > 0; --i) {
a[i] = tmp[s[i - 1] - 'a'].back();
tmp[s[i - 1] - 'a'].pop_back();
}
for (int i = 1; i <= n; ++i) p[a[i]] = i;
TreeArray A(n);
LL r = 0;
for (int i = n; i > 0; --i) {
r += A.sum(p[i]);
A.add(p[i], 1);
}
print(r);
return 0;
}

清华集训 2016 组合数问题

给定 $1 \leq n, m \leq 10^{18}$ 和素数 $p$,求所有 $0 \leq i \leq n, 0 \leq j \leq \min(i,m)$ 中有多少对 $(i, j)$ 满足 ${i \choose j}$ 是 $p$ 的倍数。

${i \choose j}$ 是 $p$ 的倍数当且仅当 $i, j$ 的 $p$ 进制中至少有一位 $x$ 满足 $i_x < j_x$(利用 Lucas 定理显然)。

所以最终答案就是

其中 $k = \max(0, n - m)$,$f(n, m)$ 表示 $i$ 的 每一位都满足 $i_x \geq j_x$ 的方案数,注意到此时必有 $i > j$,所以这里的 $i, j$ 限制分别是 $0 \leq i \leq n, 0 \leq j \leq m$. 并且可以让 $m = \min(m, n)$,我们可以从最低位开始 DP 就好了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
const LL M = 1e9 + 7;
LL inv2 = (M + 1) / 2;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas, p;
std::cin >> cas >> p;
while (cas--) {
LL n, m;
std::cin >> n >> m;
m = std::min(m, n);
LL r = (2 * n + 2 - m) % M * ((m + 1) % M) % M * inv2 % M;
std::vector<int> an, bm;
auto digit = [&]() {
int r = 0;
while (n) {
an.emplace_back(n % p);
bm.emplace_back(m % p);
n /= p;
m /= p;
++r;
}
return r;
};
int d = digit();
// dp[i][sa][sb]; sa, sb 分别表示 $a, b$ 第 $i$ 为是否有限制
LL dp[d + 1][2][2];
for (int i = 0; i < 4; ++i) dp[0][i / 2][i % 2] = 1;
for (int i = 0; i < d; ++i) {
dp[i + 1][0][0] = (p + 1) * p / 2 * dp[i][0][0] % M;
dp[i + 1][0][1] = ((2 * p - bm[i] + 1) * bm[i] / 2 * dp[i][0][0] + (p - bm[i]) * dp[i][0][1]) % M;
dp[i + 1][1][0] = ((an[i] + 1) * an[i] / 2 * dp[i][0][0] + (an[i] + 1) * dp[i][1][0]) % M;
dp[i + 1][1][1] = 0;
if (an[i] >= bm[i]) dp[i + 1][1][1] += (an[i] - bm[i]) * dp[i][0][1] + dp[i][1][1];
else dp[i + 1][1][1] += dp[i][1][0];
bm[i] = std::min(bm[i], an[i]);
(dp[i + 1][1][1] += (2 * an[i] + 1 - bm[i]) * bm[i] / 2 * dp[i][0][0] + bm[i] * dp[i][1][0]) %= M;
}
r = (r + M - dp[d][1][1]) % M;
print(r);
}
return 0;
}

清华集训 2016 求和

$f$ 是一个次数不超过 $m$ 的多项式,满足 $f(i) = a_i, i = 0, \cdots, m$,求

给定 $n, m, x$ 和 $a_0, \cdots, a_m$,其中 $1 \leq n \leq 10^9, 1 \leq m \leq 2 \cdot 10^4, 0 \leq a_i, x \leq 998244353$

做法:利用二项式反演,记 $f(k) = \sum_{i = 0}^k {k \choose i} f_i$,则 $f_k = \sum_{i = 0}^k {k \choose i} (-1)^{k-i} f(i)$ 注意到 $f(x)$ 是次数不超过 $m$ 的多项式,所以 $f_{m + 1} = f_{m + 2} = \cdots = 0$

上述式子最后一项,只有 $m + 1$ 项,$f_0, \cdots, f_m$ 可以由 NTT 计算。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

const int N = 2e4 + 2;
const LL M = 998244353;
const LL ROOT = 3;
LL powMod(LL x, LL n) {
LL r(1);
while (n) {
if (n & 1) r = r * x % M;
n >>= 1;
x = x * x % M;
}
return r;
}
LL fac[N], ifac[N];
void init() {
fac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = fac[i - 1] * i % M;
ifac[N - 1] = powMod(fac[N - 1], M - 2);
for (int i = N - 1; i; --i) ifac[i - 1] = ifac[i] * i % M;
}
void bitreverse(std::vector<LL> &a) {
for (int i = 0, j = 0; i != a.size(); ++i) {
if (i > j) std::swap(a[i], a[j]);
for (int l = a.size() >> 1;
(j ^= l) < l; l >>= 1);
}
}
void ntt(std::vector<LL> &a, bool isInverse = false) {
LL g = powMod(ROOT, (M - 1) / a.size());
if (isInverse) {
g = powMod(g, M - 2);
LL invLen = powMod(LL(a.size()), M - 2);
for (auto & x: a) x = x * invLen % M;
}
bitreverse(a);
std::vector<LL> w(a.size(), 1);
for (int i = 1; i != w.size(); ++i) w[i] = w[i - 1] * g % M;
auto addMod = [](LL x, LL y) {
return (x += y) >= M ? x -= M : x;
};
for (int step = 2, half = 1; half != a.size(); step <<= 1, half <<= 1) {
for (int i = 0, wstep = a.size() / step; i != a.size(); i += step) {
for (int j = i; j != i + half; ++j) {
LL t = (a[j + half] * w[wstep * (j - i)]) % M;
a[j + half] = addMod(a[j], M - t);
a[j] = addMod(a[j], t);
}
}
}
}
void mul(std::vector<LL>& a, std::vector<LL> b) {
int sz = 1, tot = a.size() + b.size() - 1;
while (sz < tot) sz *= 2;
a.resize(sz);
b.resize(sz);
ntt(a);
ntt(b);
for (int i = 0; i != sz; ++i) a[i] = a[i] * b[i] % M;
ntt(a, 1);
a.resize(tot);
}

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
init();
int m, n, x;
std::cin >> n >> m >> x;
std::vector<LL> a(m + 1), b(m + 1);
for (int i = 0; i <= m; ++i) {
std::cin >> a[i];
a[i] = ifac[i] * a[i] % M;
b[i] = (i % 2 == 0) ? ifac[i] : M - ifac[i];
}
mul(a, b);
a.resize(m + 1);
LL r = 0, Anx = 1;
for (int i = 0; i <= m; ++i) {
r += Anx * a[i] % M;
Anx = Anx * (n - i) % M * x % M;
}
print(r % M);
return 0;
}

ZOJ 3820:树的重心 + 直径

题意:在一棵树上选择两个点,使得任意点到这两点的最小值的最大值最小。

如果仅仅选择一个点,那么这个问题就是树的重心。

对此情况,将直径按照中点切开,然后分别求两颗子树的中心, 参考资料

证明:如果 u,v 是满足条件的两个点,那么把他们沿着中间切开必然最优,然后反证它们在树的直径上。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int cas;
std::cin >> cas;
while (cas--) {
int n;
std::cin >> n;
std::vector<std::vector<int>> e(n);
for (int i = 1, u, v; i < n; ++i) {
std::cin >> u >> v;
--u; --v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
std::vector<int> d(n);
auto bfs = [&](int x) -> int {
std::fill(d.begin(), d.end(), -1);
std::queue<int> Q;
d[x] = 0;
Q.push(x);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for (auto v : e[u]) if (d[v] == -1) {
d[v] = d[u] + 1;
Q.push(v);
}
}
return std::max_element(d.begin(), d.end()) - d.begin();
};
auto f = [&](int v) {
std::vector<int> a;
a.emplace_back(v);
while (d[a.back()]) {
for (auto v: e[a.back()]) if (d[v] + 1 == d[a.back()]) {
a.emplace_back(v);
break;
}
}
int mid = a.size() / 2;
return std::make_pair(a[mid - 1], a[mid]);
};
auto [u, v] = f(bfs(bfs(0)));
e[u].erase(std::find(e[u].begin(), e[u].end(), v));
e[v].erase(std::find(e[v].begin(), e[v].end(), u));

int ru = bfs(bfs(u)), ansu = (d[ru] + 1) / 2;
auto ra = f(ru).second;
int rv = bfs(bfs(v)), ansv = (d[rv] + 1) / 2;
auto rb = f(rv).second;
std::cout << std::max(ansu, ansv) << " " << ru + 1 << " " << rv + 1 << std::endl;
}
return 0;
}

1422D:堆优化 Dijkstra 复杂度 $O(n \log E)$

这里的边数 E 针对本问题可以大大优化,代码可参考 Jiangly 的代码

1422C

一开始读题读错了,少看了一个连续导致浪费了很多时间(30 分钟)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
const LL M = 1e9 + 7;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::string a;
std::cin >> a;
LL r = 0, n = 0, id = 1, r2 = 0;
for (auto x : a) {
r = (r * 10 + id * (id - 1) / 2 * (x - '0') + r2) % M;
n = (n * 10 + (x - '0')) % M;
r2 += n;
++id;
}
print(r);
return 0;
}

Atcoder ARC 104D

E, F 题也是好题,不过不懂。

问题描述:对每一个 $m \in [1, n]$ 求满足 $\sum_{x \in S} (x - m) = 0$ 的集合 $S$ 的个数,其中 “集合” $S$ 是由 1~n 中元素构成,元素可重,重数不超过 k。这等价于说 $\sum_{x \in S} x = \sum_{x \in T} x$ 的个数乘以 $k + 1$,其中 $S$ 是由 1 ~ m - 1 构成,$T$ 由 1 ~ n - m 构成。

做法:我们设 dp[i][j] 表示仅用 1 ~ i 中的数构成和为 j 的个数,那么显然

于是我们保存一下前缀和,那么就可以优化计算了。

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
LL M;
std::cin >> n >> k >> M;
std::vector<std::vector<LL>> dp(n);
dp[0].emplace_back(1);
for (int i = 1; i < n; ++i) {
int mx = (i + 1) * i / 2 * k;
dp[i] = dp[i - 1];
dp[i].resize(mx + 1);
for (int j = i; j <= mx; ++j) dp[i][j] += dp[i][j - i];
for (int j = mx; j >= (k + 1) * i; --j) dp[i][j] -= dp[i][j - (k + 1) * i] %= M;
for (auto &x : dp[i]) x %= M;
}
for (int x = 1; x <= n; ++x) {
LL r = 0;
int tx = std::min(x - 1, n - x);
for (int j = (tx + 1) * tx / 2 * k; j >= 0; --j) r += dp[x - 1][j] * dp[n - x][j] % M;
r = (r * (k + 1) % M + M - 1) % M;
print(r);
}
return 0;
}

1408D:二维处理问题,经典重要问题

一开始没有看数据范围觉得没法过,就没想这个问题,然后狮子大张口想做最后一题…

此问题可以转换成经典问题:二维平面,第一象限的 $n$ 个点,每次只能同时向左或者向下移动一个单位,问最少需要多少步,让所有的点都不在第一象限。
首先先对一个坐标(例如横坐标)进行从小到大排序,然后最终结果就是 第 i 个点的横坐标加上后面所有点的纵坐标的最大值,这 n + 1 个结果中最小的一个!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<std::pair<int, int>> a(n), b(m), t;
for (auto &[x, y] : a) std::cin >> x >> y;
for (auto &[x, y] : b) std::cin >> x >> y;
std::vector<int> r(n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (a[i].first <= b[j].first && a[i].second <= b[j].second) {
t.push_back({b[j].first - a[i].first + 1, b[j].second - a[i].second + 1});
}
}
}
std::sort(t.begin(), t.end());
std::vector<int> mx(t.size() + 1);
for (int i = t.size() - 1; ~i; --i) {
mx[i] = std::max(mx[i + 1], t[i].second);
}
int ans = mx[0];
for (int i = 0; i < t.size(); ++i) {
ans = std::min(ans, mx[i + 1] + t[i].first);
}
print(ans);
return 0;
}

1408F:脑筋急转弯问题

读完题目我就知道对于 $n$ 为 2 的幂次时,可以让所有元素一致,但是后面怎么就没有想到前后各搞一次呢?我在干什么!!!

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
#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
//freopen("in","r",stdin);
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::pair<int, int>> r;
auto f = [&](int x, int len) {
for (int step = 1; step < len; step *= 2) {
for (int i = 0; i < len; i += step * 2) {
for (int j = 0; j < step; ++j) {
r.push_back({i + j + x, i + j + x + step});
}
}
}
};
int k = std::__lg(n);
f(1, 1 << k);
f(n + 1 - (1 << k), 1 << k);
print(r.size());
for (auto [x, y] : r) std::cout << x << " " << y << std::endl;
return 0;
}

1408I

本题解基于 Soulist 的题解和 Jiangly 的代码,写这个的原因是这个题我是非常想搞清楚,然后只有转化成自己的语言才能弄清楚,所以想写下来,从而说明是真的理解了。

题意:给定 $n$ 个数 ($a_1, a_2, \cdots, a_n$),(序列元素互异,但是这个条件没啥用吧)每次操作