阅读视图

发现新文章,点击刷新页面。
🔲 ☆

Codeforces Round 1089 (Div. 2)

A. A Simple Sequence

大致题意

要求你生成一个排列,满足

$a_1 \space mod \space a_2 \geq a_2 \space mod \space a_3 \dots a_{n-2} \space mod \space a_{n-1} \geq a_{n-1} \space mod \space a_n$

思路

由于这里可以是 $\geq$,而 $n \space n-1 = 1$,是一个显然的等式,所以直接倒序输出即可

AC code

1
2
3
4
5
6
7
8
9
void solve() {
int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int n;
cin >> n;
for (int i = n; i >= 1; --i) cout << i << " \n"[i == 1];
}
}

B. Simply Sitting on Chairs

大致题意

有一个 $n$ 的排列 $p$,然后需要完成如下操作:

从第一个值开始往后逐个选择,如果选择了这个值 $i$,那么接下来就不能选择 $p_i$

问最多可以选多少个值

思路

其实也非常简单,只要选择的 $i \geq p_i$ 就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void solve() {
int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int n, ans = 0;
cin >> n;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
ans += x <= i + 1;
}
cout << ans << endl;
}
}

C2. A Simple GCD Problem

大致题意

有两个数组 $a, b$,都是长度 $n$,现在希望生成一个新的数组 $a’$,满足

$a’_i \in \Set{a_i, [1, b_i]}, \forall \Set{l, r} (1 \leq l < r \leq n), gcd(a_l, a_{l+1}, a_{l+2}, \dots, a_{r}) = gcd(a’_l, a’_{l+1}, a’_{l+2}, \dots, a’_{r})$

问最多可以同时存在多少个 $a’_i$ 满足 $a’_i \neq a_i$

思路

首先先分析题目中提到的 $gcd(a_l, a_{l+1}, a_{l+2}, \dots, a_{r}) = gcd(a’_l, a’_{l+1}, a’_{l+2}, \dots, a’_{r})$

看起来很吓人,实际上根据 $gcd$ 的性质,可以得到 $gcd(gcd(a, b), gcd(b, c)) = gcd(a, b, c)$

而题目中提到的是$\forall \Set{l, r} (1 \leq l < r \leq n)$,由于任意区间的 $gcd$ 等于这个区间里的相邻值的 $gcd$ 再做 $gcd$

所以要求条件可以转为: $\forall i (1 \leq i < n), gcd(a_i, a_{i+1}) = gcd(a’_i, a’_{i+1})$

要满足这条,我们需要先找出一个数组 $c$,满足 $c_i = gcd(a_i, a_{i+1})$,这不是什么难事

显然我们可以得到,最终的 $a’$ 满足: $gcd(a’_i, a’_{i-1}) = c_{i-1}, gcd(a’_i, a’_{i+1}) = c_{i+1}$

根据 $gcd$ 的性质,我们可以得到 $a’_i = x \times lcm(c_{i-1}, c_i), x \geq 1$

由此我们可以得到 $a’$ 数组的每一项的最小可选值,即 $a’_i = lcm(c_{i-1}, c_i)$

至此,我们已经完成了 Easy Version 的题解。由于 $b_i = a_i$,所以如果 $lcm(c_{i-1}, c_i) = a_i$,那么就不可能存在 $a’_{i} \neq a_{i}$

接下来是讨论 Hard 部分的解决方案

显然,如果 $lcm(c_{i-1}, c_i) \neq a_i$ 的话,我们就可以选择令 $a’_{i} = lcm(c_{i-1}, c_i)$,因为再乘上任何值都有可能让 $gcd$ 发生变化(变大)

接下来核心是要处理这些不满足的值,也就是 $lcm(c_{i-1}, c_i) = a_i$ 的值,尝试找到一个 $x$ 使得 $x \times lcm(c_{i-1}, c_i) \neq a_i$ 且不改变 $gcd$ 关系

由于不能改变 $gcd$ 关系,假定 $a’_i = x_i \times lcm(c_{i-1}, c_i)$,那么 $gcd(x_i, a’_{i-1}) = gcd(x_i, a’_{i+1}) = 1$

扩展后可以得到:

$gcd(x_i, x_{i-1}) = gcd(x_i, x_{i+1}) = gcd(x_i, lcm(c_{i-2}, c_{i-1})) = gcd(x_i, lcm(c_{i}, c_{i+1}))$

即有很多很多的互质

显然我们很容易想到用素数,因为任意两个素数之间肯定互质,由于本身是乘法,且只需要找到相互互质的值即可,所以只需要限制在较小的值内即可

我用了 100 以内的素数,通过 dp 的方式,枚举每一位乘上每一种素数的情况

我的 dp 算法里,下标 $x$ 表示第几个素数,其中 $0$ 表示 $lcm(c_{i-1}, c_i)$ 本体,而 $max(x)$ 表示 $a_i$ 本身

AC code

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
#define int long long

int gcd(const int a, const int b) {
if (b == 0) return a;
return gcd(b, a % b);
}

void solve() {
vector<int> primes;
primes.push_back(1);
vector isPrime(100, true);
for (int i = 2; i < 100; i++) {
if (isPrime[i]) {
primes.push_back(i);
}
for (int j = i * i; j < 100 && j > 0; j += i) {
isPrime[j] = false;
}
}

int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int n;
cin >> n;
vector<int> a(n), b(n), c(n);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) cin >> b[i];

for (int i = 0; i < n; i++) {
if (i == 0) {
c[i] = gcd(a[0], a[1]);
} else if (i == n - 1) {
c[i] = gcd(a[i - 1], a[i]);
} else {
const int x = gcd(a[i - 1], a[i]);
const int y = gcd(a[i] , a[i + 1]);
c[i] = x * y / gcd(x, y);
}
}
vector<int> dp[2];
dp[0].resize(primes.size() + 1);
dp[1].resize(primes.size() + 1);
int cur = 0, nxt = 1, ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < primes.size() + 1; ++j) {
const int v = j == primes.size() ? a[i] : c[i] * primes[j];
dp[nxt][j] = -1;
if (v > b[i] && v != a[i]) continue;
if (i == 0) {
dp[nxt][j] = v != a[i];
continue;
}

for (int k = 0; k < primes.size() + 1; ++k) {
const int u = k == primes.size() ? a[i - 1] : c[i - 1] * primes[k];
if (gcd(u, v) == gcd(c[i - 1], c[i])) {
dp[nxt][j] = max(dp[nxt][j], dp[cur][k] + (v != a[i]));
}
}
}
swap(cur, nxt);
}
for (auto x : dp[cur]) ans = max(ans, x);
cout << ans << endl;
}
}
🔲 ☆

Codeforces Round 1079 (Div. 2)

很久没有写题了,最近也想稍微恢复一下自己的脑子,经常让 AI 帮忙写也容易脑子生锈,特别是今天这场,一个明显的暴力题却卡了很久

A. Friendly Numbers

大致题意

已知一个等式 $y - d(y) = x$,其中的 $d(y)$ 的意思是将 $y$ 的十进制的每一位求和。问给出 $x$ 的情况下有多少个可能的 $y$

思路

这道题是比较容易解决的,虽然 $x$ 和 $y$ 都有可能很大,不适合操作,但是 $d(y)$ 的范围是有限的,显然最大不可能超过 $90$,因为 $1 \leq x \leq 10^9$

那么我们枚举可能的 $d(y)$ 然后根据原等式得到 $y = x + d(y)$ 即可得到 $y$,再验证 $y$ 是不是一个合法的值(即 $d(y)$ 是否等于我们枚举的值即可)

AC code

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
bool sum(int x, int len, int target) {
while (x) {
--len;
target -= x % 10;
x /= 10;
}
return !len && !target;
}

void solve() {
int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int n;
cin >> n;
// get n length
int len = 0, tmp = n;
while (tmp) {
++len;
tmp /= 10;
}

// test for len
int ans = 0;
for (int i = 0; i <= len * 9; ++i)
ans += sum(n + i, len, i);
for (int i = 0; i <= (len + 1) * 9; ++i)
ans += sum(n + i, len + 1, i);

cout << ans << endl;
}
}

B. Array and Permutation

大致题意

有两个长度都为 $n$ 的数组 $p$ 和 $a$,其中 $p$ 是 $n$ 的排列,现在允许你通过任意次以下操作,检查 $p$ 是否能够变成 $a$

  • 选择 $p$ 中的一个值,将这个值拷贝给它的其中一个相邻的值

思路

显然,因为值的移动是通过将沿途的值都覆盖掉的方式进行的,例如如下的方式就会无法达成

1
2
3
4
5
6
7
1  2  3
\ /
\ /
X
|\
| \
x 3 1

因为 1 需要移动到第三个位置的时候,必然第 1、2、3 个位置都会变成 1,即第三个位置的 3 就会被覆盖掉。而因为 $p$ 是 $n$ 的排列,所以一旦被覆盖了 3 之后,就再也无法创造 3 出来了

所以核心的思路是:如果 $p$ 一个值在 $a$ 中出现,那么这个值在 $p$ 中的左边的值,不可以再次出现,不然就会出现上面的交叉

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void solve() {
int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int n;
cin >> n;
vector<int> p(n), a(n);
for (int i = 0; i < n; ++i) cin >> p[i];
for (int i = 0; i < n; ++i) cin >> a[i];

int t = 0;
bool ans = true;
for (int i = 0; i < n; ++i) {
while (t < n && p[t] != a[i]) ++t;
if (p[t] != a[i]) ans = false;
}

cout << (ans ? "YES" : "NO") << endl;
}
}

C. Game with a Fraction

大致题意

Alice 和 Bob 的博弈。有两个数字 $p, q$,现在每一轮要求他们选择其中一个值,并将其减去 $1$,Alice 先手。
如果在某一时刻出现 $\frac{p}{q} = \frac{2}{3}$ 的时候,认为 Bob 胜出,否则 为 Alice 胜出

思路

除开开局胜利的情况,Bob 只有一种可能的胜利办法:$\frac{2x + n}{3x + n}$ 给到 Alice 的时候,Alice 无法阻止生成一个 $\frac{2}{3}$,
因为 Bob 只需要根据 Alice 的操作,操作另外一个数值即可

而只要给到 Alice 的不是这种场景,Alice 都可以很容易避免出现,例如 $\frac{2x}{3x + 1}$ 给到 Alice,Alice 可以选择让 $p - 1$ 来避开必败逻辑

所以必须开局的数值满足上面的公式才行

AC code

1
2
3
4
5
6
7
8
9
10
11
12
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int p, q;
cin >> p >> q;
int x = p * 3 - 2 * q;
cout << (x >= 0 && p - x >= 2 ? "Bob" : "Alice") << endl;
}
}

D. Another Problem about Beautiful Pairs

大致题意

有一个数组,要求你找有多少对下标满足 $a_i \times a_j = j - i$

思路

其实就是个暴力题,遍历每一个位置,为这个位置寻找可能存在配对的值即可

注意这道题需要考虑一个非常巧妙的点:
正常情况下,这段代码类似埃氏筛,外层 for (int i = 2; i < n; ++i) 内层 for (int j = i; i * J < n; ++j) 这样。
当然这里是用类似埃氏筛的代码举例,实际可以参考下面的代码,直接使用有的值,而不是遍历所有可能的 j
但是这道题却要反过来,即外层 for (int i = 2; i < n; ++i) 内层 for (int j = 2; j <= i; ++j) 这样。

这个问题主要在于,对于比较小的值,前者的写法内循环次数会更高,而后者的写法可以有效降低内循环次数。

至于为啥对于较大的数值,并不会有较大影响呢?很简单,因为对于 1 而言,其需要固定遍历所有值,而对于超过 $\frac{n}{x}$ 的值而言,之多只需要遍历 $x$ 个值即可

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 1; ts <= _; ++ts) {
int n;
cin >> n;
vector<int> a(n);
set<int> cnt;
for (int i = 0; i < n; ++i) {
cin >> a[i];
cnt.insert(a[i]);
}

int ans = 0;
for (auto i = 0; i < n; ++i) {
const auto end = cnt.upper_bound(a[i]);
for (auto j = cnt.begin(); j != end; ++j) {
const int v = a[i] * *j;
if (v >= n) break;
if (i - v >= 0 && a[i - v] == *j) ++ans;
if (i + v < n && a[i + v] == *j && a[i] != *j) ++ans;
}
}

cout << ans << endl;
}
}
🔲 ☆

The Burrows-Wheeler Transform 块排序压缩算法

前言

最近发现了一个非常有意思的算法:The Burrows-Wheeler Transform(块排序压缩算法),所以稍微花点时间简单记录一下

说是一个压缩算法,实际上这个算法主要做的事情是把数据重排序,实际上并没有减少数据的长度(通常反而因为增加了行尾标识而增长了)

但是它完成了一个非常有意思的效果:在几乎不增加字符串长度的情况下,把一个字符串的重复的部分尽可能聚合到一块了

而 Gzip 压缩算法基于Deflate算法,通过结合LZ77算法(查找并替换重复字符串)和霍夫曼编码,通过将接近的字符串聚合到一块,能够有效增加压缩率

算法操作方式

我做了一个简单的演示工具,我们就用经典的 banana 作为示例

在原串后添加结束结束符号$,且此符号认为是最小的字符
banana\$
生成字符串的全部循环序列
banana\$
anana\$b
nana\$ba
ana\$ban
na\$bana
a\$banan
\$banana
将这几个字符串排序
\$banana
a\$banan
ana\$ban
anana\$b
banana\$
na\$bana
nana\$ba
取出最后一列字符串
\$banana
a\$banan
ana\$ban
anana\$b
banana\$
na\$bana
nana\$ba
得到结果
annb\$aa
下面将进行还原操作
annb\$aa
将结果排成一列
a
n
n
b
\$
a
a
排序
\$
a
a
a
b
n
n
在当前的列之前添加 BWT 结果
a\$
na
na
ba
\$b
an
an
再次排序
\$b
a\$
an
an
ba
na
na
重复上述步骤
a\$b
na\$
nan
ban
\$ba
ana
ana
再次排序
\$ba
a\$b
ana
ana
ban
na\$
nan
重复上述步骤
a\$ba
na\$b
nana
bana
\$ban
ana\$
anan
再次排序
\$ban
a\$ba
ana\$
anan
bana
na\$b
nana
重复上述步骤
a\$ban
na\$ba
nana\$
banan
\$bana
ana\$b
anana
再次排序
\$bana
a\$ban
ana\$b
anana
banan
na\$ba
nana\$
重复上述步骤
a\$bana
na\$ban
nana\$b
banana
\$banan
ana\$ba
anana\$
再次排序
\$banan
a\$bana
ana\$ba
anana\$
banana
na\$ban
nana\$b
重复上述步骤
a\$banan
na\$bana
nana\$ba
banana\$
\$banana
ana\$ban
anana\$b
再次排序
\$banana
a\$banan
ana\$ban
anana\$b
banana\$
na\$bana
nana\$ba
回到最初的矩阵
\$banana
a\$banan
ana\$ban
anana\$b
banana\$
na\$bana
nana\$ba

算法原理

觉得这个算法有意思的地方,可能并不是它的实用价值。这里有一个非常有意思:为什么这样排序了几次之后,就会回到最初的矩阵

这里蕴藏了一个非常有意思的字符串排序逻辑。

通常情况下,我们会使用字符串从第一个字符开始比较,如果相同则比下一个字符

而在这个问题下,假定所有字符串长度相同,那其实完全可以从最后一位比起,然后逐次比较新增加的字符,可以实现类似桶排序的方式,达成最终排序结果

由于后排序的结果会覆盖先排序的结果,使得实际上达成了“从第一个字符开始比较,如果相同则比下一个字符”的效果

☑️ ☆

Codeforces Round 942 (Div. 2)

A. Contest Proposal

大致题意

有两个数组 $a, b$,已经从小到大排序好了,现在往 $a$ 数组最前面再塞入几个值,同时从最后面删除相同数量的值,使得 $\forall i \in [1, n], a_i \leq b_i$

思路

简单题,由于数据量很小,甚至可以暴力

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void solve() {
int n;
cin >> n;
vector<int> a(n), b(n);
for (auto &i: a) cin >> i;
for (auto &i: b) cin >> i;
int l = 0, r = 0, ans = 0;
while (l < n && r < n) {
if (a[l] <= b[r]) {
++l;
++r;
} else {
++ans;
++r;
}
}
cout << ans << endl;
}

B. Coin Games

大致题意

有两个人做游戏,有几个英镑再桌面上,有些正面朝上有些背面朝上。

每次操作,允许移走一个正面朝上的,然后连续选择两个剩下的影片进行翻转

问谁会操作到最后一次

思路

翻转两个硬币等于没有翻转

AC code

1
2
3
4
5
6
7
8
9
10
void solve() {
int n;
cin >> n;
string str;
str.resize(n);
cin >> str;
int cnt = 0;
for (const auto& c: str) cnt += c == 'U';
cout << (cnt % 2 ? "YES" : "NO") << endl;
}

C. Permutation Counting

大致题意

有 $n$ 种卡片,每种都有一定数量,现在允许额外再增加 $k$ 张,使得这些卡片可以组成一个数组,数组种的存在的 $[1, n]$ 的排列的子串尽可能多,问可以有多少个

思路

只需要 $1, 2, 3, \dots, n, 1, 2, 3, \dots n$ 类似这样排列即可,通过二分找出每个数值都能到达的数量,然后排列起来

然后是剩下的那部分,比如多了一个 $1$,那么按照上面的排列方式,将 $1$ 放在最后面还能再多一次,即每有一个多出来的种类,就能增加一个子串

AC code

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
#define int long long

void solve() {
int n, k;
cin >> n >> k;
vector<int> data(n);
for (auto &i: data) cin >> i;
auto check = [&](int x) {
int use = 0;
for (const auto &v: data) {
if (v < x) use += x - v;
if (use > k) break;
}
return use <= k;
};

int l = 0, r = 1e15;
while (l + 1 < r) {
int mid = (l + r) >> 1;
if (check(mid)) l = mid;
else r = mid;
}
int ans = 0, use = k;
for (const auto& v: data) {
if (v > l) ++ans;
else use -= l - v;
}
ans = min(n, use + ans);

cout << ans + (l - 1) * n + 1 << endl;
}

D1. Reverse Card (Easy Version)

大致题意

给出 $n, m$,求满足条件的 $a, b$ 对

  • $1 \leq a \leq n, 1 \leq b \leq m$
  • $(a + b) \space mod \space b \times gcd(a, b) = 0$

思路

假定 $a = x \times y, b = x \times z$,且 $gcd(y, z) = 1$
则可以得到

$$ & (a + b) \space mod \space b \times gcd(a, b) = 0 \\\rightarrow & x \times y + x \times z = t \times (x \times z \times x) \\\rightarrow & y + z = t \times x \time z \\\rightarrow & 1 + \frac{y}{z} = t \times x \\$$

容易得到,必然 $\frac{y}{z}$ 是整数,而 $gcd(y, z) = 1$,所以 $z = 1$,故 $1 + y = t \times x$

所以很容易得到公式进行计算

AC code

1
2
3
4
5
6
7
8
9
10
11
#define int long long

void solve() {
int n, m, ans = 0;
cin >> n >> m;
for (int i = 1; i <= n && i <= m; ++i) {
int my = n / i;
ans += (my + 1) / i;
}
cout << ans - 1 << endl;
}

D2. Reverse Card (Hard Version)

大致题意

给出 $n, m$,求满足条件的 $a, b$ 对

  • $1 \leq a \leq n, 1 \leq b \leq m$
  • $b \times gcd(a, b) \space mod \space (a + b) = 0$

思路

假定 $a = x \times y, b = x \times z$,且 $gcd(y, z) = 1$
则可以得到

$$ & b \times gcd(a, b) \space mod \space (a + b) = 0 \\\rightarrow & x \times z \times x = t \times (x \times y + x \times z) \\\rightarrow & x \times z = t \times (y + z) \\\rightarrow & x \times = \frac{t}{z} \times (y + z)$$

容易得到,必然 $\frac{y}{z}$ 是整数,而 $gcd(y, z) = 1$,所以必然只能用 $t$ 来承接除过来的 $z$,即上述公式中的表达

所以可以根据公式得到,只需要找到合理的互质数 $y, z$,即可找出有多少个 $x$ 满足条件,因为 $t$ 可以是任意值,即 $x$ 是 $y + z$ 的倍数

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define int long long

void solve() {
int n, m, ans = 0;
cin >> n >> m;
if (n < 2 || m < 2) {
cout << 0 << endl;
return;
}

function<int(int, int)> gcd = [&](int a, int b) {
return b == 0 ? a : gcd(b, a % b);
};

for (int i = 1; i * i <= n; ++i) {
for (int j = 1; j * j <= m; ++j) {
if (gcd(i, j) != 1) continue;

ans += min(n / i, m / j) / (i + j);
}
}
cout << ans << endl;
}
☑️ ☆

Codeforces Round 926 (Div. 2)

A. Sasha and the Beautiful Array

大致题意

有一个数组,现在允许你任意排序它,使得其所有的相邻对之差之和最小,问如何操作

思路

排序一下就行,这样就等于最大的那个值减去最小的那个

AC code

1
2
3
4
5
6
7
8
9
10
11
12
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
vector<int> data(n);
for (auto &i: data) cin >> i;
sort(data.begin(), data.end());
cout << data.back() - data.front() << endl;
}
}

B. Sasha and the Drawing

大致题意

有一个正方形,其上有 $4 \times n - 2$ 条对角线,现在要你染黑一些格子,使得这些对角线至少有 $x$ 个被覆盖,问最少染黑几个

思路

只要染黑第一行和最下面一行即可,显然,除了四个角落,其他几个点染了就是影响两条对角线

AC code

1
2
3
4
5
6
7
8
9
10
11
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n, m;
cin >> n >> m;
if (m <= 4 * n - 4) cout << (m + 1) / 2 << endl;
else if (m == 4 * n - 3) cout << 2 * n - 1 << endl;
else if (m == 4 * n - 2) cout << 2 * n << endl;
}
}

C. Sasha and the Casino

大致题意

在赌场赌博,已知每次可以下注任意合理的钱 $y$,赢了就收回 $k \times y$,输了就没了,且最多连续输 $x$ 场,问是否赚到任意数量的钱

思路

根据赌徒原理做,要保证你每次下注的时候,如果赢了能把之前输的钱全都赚回来,且还要多赚一点

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define int long long

void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int k, x, a;
cin >> k >> x >> a;
int ca = a, lose = 0;
bool flag = true;
for (int i = 0; i < x; ++i) {
int cur = (lose + k - 1) / (k - 1);
if (ca < cur) {
flag = false;
break;
}
ca -= cur;
lose += cur;
}
if (ca * k <= a) flag = false;
cout << (flag ? "YES" : "NO") << endl;
}
}

D. Sasha and a Walk in the City

大致题意

有一棵树,现在要选择一定数量的节点染色,使得任意两个节点之间的路径最多只经过两个染黑节点,问如何操作

思路

树上 dp 即可,关注当前节点到所有下面的子节点中,染色数量最多的路径染色了多少个,可以枚举 1 个和 2 个的情况(0 个一定只有一种)

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
constexpr int mod = 998244353;
vector<pair<int, int>> edges((n - 1) * 2);
vector<int> head(n + 1, -1);
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
edges[i << 1] = {v, head[u]};
edges[i << 1 | 1] = {u, head[v]};
head[u] = i << 1;
head[v] = i << 1 | 1;
}

function<pair<int, int>(int, int)> dfs = [&](int u, int p) {
int a = 1, b = 0;
for (int e = head[u]; ~e; e = edges[e].second) {
if (edges[e].first == p) continue;
auto [na, nb] = dfs(edges[e].first, u);
a = (a * (1 + na)) % mod;
b = (b + na + nb) % mod;
}

return make_pair(a, b);
};

auto [a, b] = dfs(1, 0);
cout << (a + b + 1) % mod << endl;
}
}
🔲 ☆

Codeforces Round 919 (Div. 2)

A. Satisfying Constraints

大致题意

给出一堆条件,包括是否大于、小于且不等于某个值,问最终有几个值符合条件

思路

先记录最小的那个区间,然后再过滤掉不满足的,就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
int l = 0, r = INT_MAX;
set<int> s;
for (int i = 0; i < n; ++i) {
int o, x;
cin >> o >> x;
if (o == 1) l = max(l, x);
else if (o == 2) r = min(r, x);
else s.insert(x);
}
int cnt = r - l + 1;
for (auto& v: s) if (v <= r && v >= l) --cnt;
cout << max(0, cnt) << endl;
}
}

B. Summation Game

大致题意

两个人博弈,一个人先移除最多 $k$ 个值,另外一个人将会把最多 $x$ 个值变成负数,问最终所有值加起来最大是多少

思路

显然,删除最大的最有利,所以枚举删除几个即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n, k, x;
cin >> n >> k >> x;
vector<int> data(n);
for (auto& i: data) cin >> i;
sort(data.begin(), data.end(), greater<>());
int total = 0, fx = 0;
for (int i = 0; i < n; ++i) {
total += data[i];
fx += i < x ? data[i] : 0;
}
int cur = 0, ma = 0;
for (int l = 0, r = x; l < k; ++l, ++r) {
cur -= r < n ? 2 * data[r] : 0;
cur += data[l];
ma = max(ma, cur);
}
cout << total - fx - fx + ma << endl;
}
}

C. Partitioning the Array

大致题意

一个数组,对于每个值再找一个特殊值取模,将其拆成 $n$ 等分,得到的每一个等分的数组相同,问有几种拆法

思路

拆法必然是数组长度的因子,暴力枚举即可。因为一个树的因子一定不会很多

AC code

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
int gcd(const int a, const int b) {
return b == 0 ? a : gcd(b, a % b);
}
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
vector<int> data(n);
for (auto& i: data) cin >> i;
auto check = [&](const int x) {
const int len = n / x;
int tmp = 0;
for (int i = 1; i < x; ++i)
for (int j = 0; j < len; ++j)
tmp = gcd(abs(data[i * len + j] - data[(i - 1) * len + j]), tmp);
return tmp != 1;
};
int ans = 0;
for (int i = 1; i * i <= n; ++i) {
if (n % i) continue;
ans += check(i);
if (i * i != n) ans += check(n / i);
}
cout << ans << endl;
}
}

D. Array Repetition

大致题意

开始有一个空的数组,有两种操作:

  • 往数组最后加一个元素 $x$
  • 把整个数组复制 $x$ 次

问最终的数组中,第 $i$ 位是什么

思路

最终的数值一定是第一个操作得到的,所以可以考虑不断递归逆向整个操作过程,看看目标位置最终是被哪一次加入元素带来的

AC code

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
#define int long long
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n, q;
cin >> n >> q;
vector<pair<int, int>> data(n);
for (auto& [fst, snd]: data) cin >> fst >> snd;
map<int, list<int>> mp;
int maxK = 0;
for (int i = 0; i < q; ++i) {
int tmp;
cin >> tmp;
mp[tmp].push_back(i);
maxK = max(maxK, tmp);
}
int tot = 0, i = 0;
for (; i < n; ++i) {
if (data[i].first == 1) ++tot;
else {
if ((maxK + tot - 1) / tot <= 1 + data[i].second)
data[i].second = (maxK + tot - 1) / tot;
tot *= 1 + data[i].second;
}
if (tot >= maxK) break;
}
vector<int> ans(q);
for (; i >= 0; --i) {
if (data[i].first == 1) {
if (const auto iter = mp.find(tot); iter != mp.end())
for (const auto& v: iter->second) ans[v] = data[i].second;
mp.erase(tot);
--tot;
} else {
const int len = tot / (1 + data[i].second);
for (auto iter = mp.upper_bound(len); iter != mp.end(); ) {
list<int>& l = mp[(iter->first - 1) % len + 1];
l.splice(l.end(), iter->second);
auto nxtIter = iter;
++nxtIter;
mp.erase(iter);
iter = nxtIter;
}
tot = len;
}
}
for (int i = 0; i < q; ++i) cout << ans[i] << " \n"[i == q - 1];
}
}
🔲 ☆

Codeforces Round 915 (Div. 2)

A. Constructive Problems

大致题意

有一个棋盘,允许选择一定数量的方格先进行染色

若某个方格的相邻四个格子中,横向至少有一个已经染色,且纵向至少也有一个已经染色的情况下,那么这个格子也可以被自然染色

问最少最初选择的方格数量是多少

思路

对角线即可

AC code

1
2
3
4
5
6
7
8
9
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int a, b;
cin >> a >> b;
cout << max(a, b) << endl;
}
}

B. Begginer’s Zelda

大致题意

有一棵树,允许每次选择树上的一条路径,然后将路径上的所有的点都挤压到一个点上,问最多需要挤压几次才能让整个树变成一个点

思路

其实只需要统计叶子结点数量就行了,然后两两连线挤压即可,必定存在一种方法使得整个树的所有边被遍历

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
vector<int> deg(n + 1, 0);
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
++deg[u];
++deg[v];
}

int cnt = 0;
for (int i = 1; i <= n; ++i) cnt += deg[i] == 1;
cout << (cnt + 1) / 2 << endl;
}
}

C. Largest Subsequence

大致题意

有一个字符串,接下来有如下操作

  • 找到这个字符串中字典序最大子序列
  • 将这个子序列进行右移操作,仅对序列内的值生效

问需要操作几次才能使得整个数组有序

思路

因为是字典序最大子序列,那么必然得到的子序列是一个递减的序列。

而要求是右移,即把最后面的值放到最前面,那么必然放到最前面的是最小的那个值,那么必然下一次得到字典序最大子序列的时候,必定不会包含这个值了

也就是说,实际上每次操作后,下一次得到的子序列就是上一次的子序列删掉最开头的位置和最后面的那个值,即排序操作仅对这个子序列生效

所以只需要看这个子序列需要操作几次才能有序,以及是否能够保证整个序列有序

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
string str;
str.reserve(n);
cin >> str;
vector<int> st(n);
int r = 0;
for (int i = 0; i < n; ++i) {
while (r > 0 && str[st[r - 1]] < str[i]) --r;
st[r++] = i;
}
int ans = r;
for (int i = 0; i < r; ++i) if (str[st[i]] == str[st[0]]) --ans;
--r;
for (int l = 0; l < r; ++l, --r) swap(str[st[l]], str[st[r]]);
for (int i = 1; i < n; ++i) if (str[i] < str[i - 1]) ans = -1;
cout << ans << endl;
}
}

D. Cyclic MEX

大致题意

有一个 $[0, n-1]$ 的排列,允许进行任意次的右移操作

问找到一种的排列,使得 $\sum_{i=1}^n mex([a_1, a_2, \dots a_{i}])$ 最大

思路

这种数组的 $mex$ 其实就等于要求算的那个值后面的值中最小的那个值

考虑每次右移带来的效果

  • 首先是最前面的那个 $mex([a_1])$ 会被删除掉
  • 然后影响从最后一个开始,找到第一个比当前值更小的值,这期间的所有值带来的贡献都变成当前值
  • 然后再加上固定 $n$ 的贡献

所以可以考虑单调栈的方式去做

但是我觉得这个方案有点累,所以直接用线段树了。虽然说是仅影响了更小的那个值以及后面的值
但是要明确的是,那个更小的值前面的带来的贡献,必然小于等于那个更小的值,所以只需要全局把贡献降低到当前值即可

用样例举个例子

index12345678
start23670145
mex00001458
rotate36701452
mex00012228
  • 可以看到,首先是 $2$ 移动到后面去了,贡献变成了 $8$
  • 然后是可以注意到,因为 $2$ 在最后面,所以所有值的贡献是不可能超过 $2$ 的,因为它必然是所有值后面的值

所以直接用线段树暴力即可,注意做好懒处理

AC code

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
#define ll long long

struct SegTree {
vector<ll> sum, ma, mi;
vector<bool> lazy;

explicit SegTree(const int n) : sum((n << 1) + 10), ma((n << 1) + 10), mi((n << 1) + 10), lazy((n << 1) + 10) {}

static int get(const int l, const int r) { return (l + r) | (l != r); }

void up(const int l, const int r) {
const int mid = (l + r) >> 1;
const int i = get(l, r), li = get(l, mid), ri = get(mid + 1, r);
sum[i] = sum[li] + sum[ri];
ma[i] = max(ma[li], ma[ri]);
mi[i] = min(mi[li], mi[ri]);
}

void push(const int l, const int r) {
const int mid = (l + r) >> 1;
const int i = get(l, r), li = get(l, mid), ri = get(mid + 1, r);
lazy[i] = false;
lazy[li] = true;
lazy[ri] = true;
sum[li] = (mid - l + 1) * mi[i];
sum[ri] = (r - mid) * mi[i];
ma[li] = ma[ri] = ma[i];
mi[li] = mi[ri] = mi[i];
}

void update(const int l, const int r, const int x, const ll v) {
const int i = get(l, r);
if (l == r) {
sum[i] = ma[i] = mi[i] = v;
return;
}

if (lazy[i]) push(l, r);
const int mid = (l + r) >> 1;
if (x <= mid) update(l, mid, x, v);
else update(mid + 1, r, x, v);
up(l, r);
}

void update(const int l, const int r, const ll v) {
int i = get(l, r);
if (ma[i] <= v) return;
if (mi[i] > v) {
ma[i] = mi[i] = v;
sum[i] = (r - l + 1) * v;
lazy[i] = true;
return;
}

if (l == r) {
sum[i] = ma[i] = mi[i] = v;
return;
}

const int mid = (l + r) >> 1;
update(l, mid, v);
update(mid + 1, r, v);
up(l, r);
}
};

void solve() {
int _;
cin >> _;
for (int tc = 0; tc < _; ++tc) {
int n;
cin >> n;
vector<int> data(n);
for (auto& i: data) cin >> i;

vector flag(n + 1, false);
int l = 1, r = n;
SegTree tree(r);
// init
int cur = 0;
for (int i = 0; i < n; ++i) {
flag[data[i]] = true;
while (flag[cur]) ++cur;
tree.update(l, r, i + 1, cur);
}
ll ans = 0;
for (int i = 0; i < n; ++i) {
tree.update(l, r, data[i]);
tree.update(l, r, i + 1, n);

ans = max(ans, tree.sum[tree.get(l, r)]);
}
cout << ans << endl;
}
}
🔲 ☆

Educational Codeforces Round 159 (Rated for Div. 2)

A. Binary Imbalance

大致题意

有一个 01 字符串,每次允许选择两个相邻的字母中间插入一个 01 字符,如果相邻的两个字母不同则插入 0 否则插入 1

问是否可能经过任意次数操作后,字符串中的 01

思路

只要有 0 就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
string str;
str.reserve(n);
cin >> str;
int cnt[2] = {0, 0};
for (const auto& c: str) ++cnt[c - '0'];
cout << (cnt[0] ? "YES" : "NO") << endl;
}
}

B. Getting Points

大致题意

有 $n$ 天的学期,每一天都可以选择是学习还是休息,学习的话,会得到 $l$ 分,同时可以完成至多两个任务,每个任务可以得到 $t$ 分

任务每隔 $7$ 天会生成一个,第一个任务在第 $1$ 天生成,每个任务一旦被完成就不能再次得到分数

问在至少得到 $p$ 分的情况下,最多可以休息多少天

思路

因为任务每 $7$ 天才有一个,而每天可以完成 $2$ 个,所以必然可以把任务做完。即然要休息时间足够久,那么必然是最后几天完成任务即可

所以只需要考虑最后需要多少天进行学习即可。

当然也可以考虑分类讨论,比如所有天都是完成两个任务的学习,或者个别天是不做任何任务的学习

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, p, l, t;
cin >> n >> p >> l >> t;
const int cnt = (n + 6) / 7, d = (cnt + 1) / 2;
if (d * l + t * cnt >= p)
cout << n - ((2 * t + l + p - 1) / (2 * t + l)) << endl;
else
cout << n - ((p - cnt * t + l - 1) / l) << endl;
}
}

C. Insert and Equalize

大致题意

有一个初始的数组,每个值不同,需要往里面加入一个任选的值,让数组仍然保持不同

然后进行 $t$ 次操作,每次操作是选择一个值并将其加上 $x$,问让所有值都相同的,最少多少的 $t$

思路

先不管加入一个值这个事情,如果只是纯粹的做加法,那么非常简单,只需要找到所有差值的最大公约数就行了,那么这个公约数就是 $x$

那么也就可以得到 $t$ 了,即所有值变成 $a_{max}$ 所需要的步数

接下来是加入一个值的部分,因为必须要和原来的数组不同,所以可以考虑尝试 $a_{max} - c \times x$,而 $c$ 就是需要额外增加的成本,所以要尽可能小即可

AC code

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
#define int long long

int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> data(n);
for (auto& i: data) cin >> i;
if (n == 1) {
cout << 1 << endl;
continue;
}
sort(data.begin(), data.end());
set<int> dif;
for (int i = 0; i < n - 1; ++i) dif.insert(data[i + 1] - data[i]);
int g = dif.begin().operator*();
for (const auto& s: dif) g = gcd(g, s);
int ans = 0;
for (const auto& i: data) ans += (data[n - 1] - i) / g;
if (dif.size() == 1) ans += n;
else {
for (int i = n - 2; i >= 0; --i) {
if (const int tmp = n - 1 - i; data[i] + tmp * g != data[n - 1]) {
ans += tmp;
break;
}
}
}
cout << ans << endl;
}
}

E. Collapsing Strings

大致题意

有一堆的字符串,使用 $\left | x \right |$ 表示 $x$ 的字符串长度

定义一个函数,$C(a, b)$,其中 $a, b$ 都是字符串

$$C(a, b) =\left\{\begin{matrix}a, & \left | b \right | = 0 \\b, & \left | a \right | = 0 \\C(a_{1 \dots \left | a \right | - 1}, b_{2 \dots \left | b \right |}), & a_{\left | a \right |} = b_1 \\a + b, & others\end{matrix}\right.$$

$$\sum_{i=1}^{n} \sum_{j=1}^{n} \left | C(s_i, s_j) \right |$$

思路

其实就是要找出任意两个字符串之间,前后重叠的部分即可

可以考虑用字典树做,每个字典树的节点上记录下当前节点有多少字符串在这里结束了,又有多少字符串经过了这个节点,总共有多少个字符在其子节点上即可

然后再扫一遍全部的字符串即可

AC code

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
void solve() {
int n;
cin >> n;
string str;
str.resize(1e6 + 10, -1);

struct node {
int arr[26]{};
int len = 0, cnt = 0, end = 0;

node() { memset(arr, -1, sizeof arr); }
};
vector<node> tree(1e6 + 10);
constexpr int root = 0;
int nxt = 1;

vector<string> data;
for (int i = 0; i < n; ++i) {
cin >> str;
int cur = root;
for (int l = 0; l < str.size(); ++l) {
if (!~tree[cur].arr[str[l] - 'a']) {
tree[cur].arr[str[l] - 'a'] = nxt++;
}
cur = tree[cur].arr[str[l] - 'a'];
tree[cur].len += static_cast<int>(str.size()) - l;
++tree[cur].cnt;
}
++tree[cur].end;
data.push_back(str);
}

long long ans = 0;
for (int i = 0; i < n; ++i) {
string &s = data[i];
int cur = root;
for (int l = static_cast<int>(s.size()) - 1; l >= 0; --l) {
for (int x = 0; x < 26; ++x) if (x != s[l] - 'a' && tree[cur].arr[x] != -1)
ans += tree[tree[cur].arr[x]].len + 1LL * (l + 1) * tree[tree[cur].arr[x]].cnt;
ans += 1LL * (l + 1) * tree[cur].end;
cur = tree[cur].arr[s[l] - 'a'];
if (!~cur) break;
}
if (cur != -1) for (const int x : tree[cur].arr) if (x != -1) ans += tree[x].len;
}
cout << ans << endl;
}
🔲 ☆

Codeforces Round 912 (Div. 2)

A. Halloumi Boxes

大致题意

有一个数组,每次允许选择其中一段长度不超过 $k$ 的字串进行翻转,问是否可能将其排序好

思路

最有用的翻转就是两个值,那就是冒泡了,所以要么本身有序,要么 $k \geq 2$ 就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n >> k;
vector<int> data(n);
for (auto& i: data) cin >> i;
bool sorted = true;
for (int i = 1; i < n; ++i) if (data[i] < data[i - 1]) sorted = false;
cout << (sorted || k >= 2 ? "YES" : "NO") << endl;
}
}

B. StORage room

大致题意

有一个矩阵,其中的每一项 $M_{i,j} = a_i | a_j$,问是否能得到原始数组的其中一种可能

思路

即然是 $M_{i,j} = a_i | a_j$,那么反过来可以得到 $a_i = M_{i,0} & M_{i,1} & M_{i,2} \dots$

这不一定是原始解,但是一定是正确的解。还原后再验证一下矩阵对不对就行了

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<vector<int>> data(n);
for (auto& i: data) i.resize(n);
for (auto& i: data) for (auto& j: i) cin >> j;
vector ans(n, -1);
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
if (j != i) ans[i] &= data[i][j];
bool check = true;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i == j) continue;
if (data[i][j] != (ans[i] | ans[j])) check = false;
}
}
if (!check) cout << "NO" << endl;
else {
cout << "YES" << endl;
for (int i = 0; i < n; ++i) cout << (ans[i] == -1 ? 1 : ans[i]) << " \n"[i == n -1];
}
}
}

C. Theofanis’ Nightmare

大致题意

一个数组,其中有负值,将其拆成多个字串,然后按照原始顺序从左往右编号,从 1 开始编号

然后将每个字串内求和,乘上其的编号,最后再相加,问如何拆最大

思路

从最后一个值看,假如其为 $c$,那么如果其独立出去,和不独立出去,和前面的段合并,那么得到的差值就是 $c$,因为 $(n + 1) \times c - (n) \times c$

所以,如果 $c > 0$ 那么这样做是有意义的,反之则应该尽力避免拆

类推可以得到,如果当前值即之后的所有值加起来是 $< 0$,那么不要拆,反之则拆即可

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> data(n);
for (auto& i: data) cin >> i;
int sum = 0;
for (auto& i: data) sum += i;
int seg = 0, ans = 0;
for (auto& i: data) {
if (sum < 0) {
if (seg == 0) seg = 1;
ans += seg * i;
}
else {
++seg;
ans += seg * i;
}
sum -= i;
}
cout << ans << endl;
}
}

D1. Maximum And Queries (easy version)

大致题意

有一个数组,允许每次给其中一个值 $+1$,最多执行 $k$ 次,问最终得到的数组的每一项进行位与运算,问最大值可以是多少

思路

要从位运算上下思路

如果一个值的某个位不是 1,那么如果通过 $+1$ 的方式把它变成 1,带来的后果就是更低位置的都会归零

所以我们需要从高位开始枚举位置,假设把这个位置大家都变成 1,那么就可以带来结果上的增长,但是会需要消耗一定的 $k$

注意一旦消耗过 $k$,那么再试图对这个值进行累加的时候,要把低位都认为是 0

AC code

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
#define int long long

void solve() {
int n, q;
cin >> n >> q;
vector<int> data(n);
for (auto& i: data) cin >> i;
while (q--) {
int k, ans = 0;
cin >> k;
vector flag(n, false);
for (int i = 62; i >= 0; --i) {
// try this bit
int cost = 0;
const int p = 1LL << i;
vector tmp(n, false);
for (int j = 0; j < n; ++j) {
if (flag[j]) cost += p;
else {
if (data[j] & p) continue;
cost += p - data[j] % p;
tmp[j] = true;
}

if (cost < 0) {
cost = k + 1;
break;
}
}
if (k >= cost) {
k -= cost;
ans += p;
for (int j = 0; j < n; ++j) flag[j] = tmp[j] | flag[j];
}
}
cout << ans << endl;
}
}

E. Geo Game

大致题意

有两个人博弈,二维屏幕上有一些点,其中一个固定为起始点,两个人轮流指定下一个点,不可以是之前选中的点,直到所有点都被走到

然后求算路径的欧几里得距离的平方和,问能否保证是偶数或者是奇数

思路

仔细思考就会发现很像是一笔画问题

如果某个点与开始点的欧几里得距离是奇数,我将其归为一类 A,另外的点为另外一类 B。那么显然,同一个类内的点相互走并不会变化结果的奇偶性。
但是不同类的路径就会变化奇偶性。非常巧的是,因为每个点都会被进入一次和出去一次,
所以理论如果要回到开始点,那么最终一定是偶数,因为一旦进入 A 类,就一定要回去 B 类,即每个 A 类的点会产生两条路径,也就是最终没有变化奇偶性

那么我们可以得到,如果 A 类点作为最后一个点,那么路径就是奇数的,否则就是偶数,然后再根据博弈再去模拟即可

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, sx, sy;
cin >> n >> sx >> sy;
set<int> data[2];
for (int i = 0; i < n; ++i) {
int u, v;
cin >> u >> v;
const int cost = abs(u - sx) + abs(v - sy);
data[cost % 2].insert(i + 1);
}

if (data[0].size() >= data[1].size()) {
// chose first
cout << "First" << endl;
for (int i = 0; i < n; ++i) {
if (data[1].empty()) {
auto iter = data[0].begin();
cout << *iter << endl;
data[0].erase(iter);
} else {
auto iter = data[1].begin();
cout << *iter << endl;
data[1].erase(iter);
}

++i;
if (i < n) {
// read
int tmp;
cin >> tmp;
data[0].erase(tmp);
data[1].erase(tmp);
}
}
} else {
// chose second
cout << "Second" << endl;
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
data[0].erase(tmp);
data[1].erase(tmp);
++i;

if (i < n) {
if (data[0].empty()) {
auto iter = data[1].begin();
cout << *iter << endl;
data[1].erase(iter);
} else {
auto iter = data[0].begin();
cout << *iter << endl;
data[0].erase(iter);
}
}
}
}
}
}
🔲 ☆

Codeforces Round 916 (Div. 3)

这场本来是想在比赛的时候写的,可惜写到一半突然有公司的电话进来了,就只好先去处理点事情了

A. Problemsolving Log

大致题意

写出 A 题需要思考 1min,写出 B 题需要思考 2min,以此类推,给出一个人每分钟在思考的题,问最终有几道题能写出来

思路

简单题,统计一下每个字母出现次数就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
string str;
str.reserve(n);
cin >> str;
map<char, int> mp;
for (auto&c: str) mp[c]++;
int ans = 0;
for (char i = 'A'; i <= 'Z'; ++i)
if (mp[i] >= i - 'A' + 1) ans++;
cout << ans << endl;
}
}

B. Preparing for the Contest

大致题意

需要构造一个长度为 n 的序列,使得其满足所有相邻两个值中,存在恰好 k 个前者大于后者的对数

思路

也是简单题,想清楚怎么构造就行。比如我的思路是把最大的 k 个值按照递增序放在后面

AC code

1
2
3
4
5
6
7
8
9
10
11
12
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n >> k;

for (int i = n - k; i > 0; --i) cout << i << " ";
for (int i = n - k + 1; i <= n; ++i) cout << i << " ";
cout << endl;
}
}

C. Quests

大致题意

有一堆问题,第一次写出来可以得到 $a_i$ 的分数,第二次及之后写出来可以得到 $b_i$ 的分数

每道题写出来的前提是前面的所有题至少都写出来一次

问最多写 $x$ 道题,最多可以得到多少分

思路

由于每道题必须要前面的所有题都写出来才能写,所以可以考虑枚举最终写到了哪道题,然后把剩下的所有写题的机会给那个 $b_i$ 最大的题即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n >> k;
vector<int> a(n), b(n);
for (auto&i: a) cin >> i;
for (auto&i: b) cin >> i;

int ma = 0, ans = 0, suf = 0;
for (int i = 0; i < min(n, k); ++i) {
suf += a[i];
ma = max(ma, b[i]);
ans = max(ans, suf + (k - i - 1) * ma);
}
cout << ans << endl;
}
}

D. Three Activities

大致题意

有三种活动,且有 $n$ 天,每一天最多参加一个活动,每一个活动最多只能有一天参加。

每个活动每天都有其他伙伴一起来参加,数量都不相同。问应该挑哪三天去参加活动,能够使得一起参加的伙伴数量最多

思路

由于总共就只有三种活动,所以只需要挑选三天。对于每一个活动而言,他们只需要考虑参加伙伴最多的那三天即可

所以在每一个活动下单独排序参加的伙伴数量,选择每个活动下伙伴最多的三天,暴力找下答案即可

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<pair<int, int>> a(n), b(n), c(n);
for (auto&i: a) cin >> i.first;
for (auto&i: b) cin >> i.first;
for (auto&i: c) cin >> i.first;
for (int i = 0; i < n; ++i) a[i].second = i;
for (int i = 0; i < n; ++i) b[i].second = i;
for (int i = 0; i < n; ++i) c[i].second = i;

sort(a.begin(), a.end(), greater<>());
sort(b.begin(), b.end(), greater<>());
sort(c.begin(), c.end(), greater<>());

int ans = 0;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
if (b[j].second == a[i].second) continue;
for (int k = 0; k < 3; ++k) {
if (c[k].second == a[i].second || c[k].second == b[j].second) continue;
ans = max(ans, a[i].first + b[j].first + c[k].first);
}
}
}
cout << ans << endl;
}
}

E2. Game with Marbles (Hard Version)

大致题意

直接看 Hard Version

Alice 和 Bob 做游戏,有 $n$ 种颜色的石头,每个人都有所有颜色的石头若干个

每轮,当前的选手需要选择一种颜色,然后当前选手丢掉一个这种颜色的石头,对方丢掉全部的

目标是让自己的石头数量之和尽可能多,问最后剩下多少个

思路

假如一种颜色的石头,Alice 有 $a$ 个,Bob 有 $b$ 个。如果是 Alice 选择,那么 Alice 会剩下 $a-1$ 个,差值是 $a-1$。如果是 Bob 选择,则是 $-(b-1)$

这样可以得到,不选择这个颜色的代价就是 $(a-1)-(-(b-1)) = (a+b-2)$,同时选择它的收益也是这个

按照代价排序,从最大的代价依次开始操作即可

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> a(n), b(n);
for (auto&i: a) cin >> i;
for (auto&i: b) cin >> i;
vector<pair<int, int>> d(n);
for (int i = 0; i < n; ++i) {
d[i].first = a[i] + b[i] - 2;
d[i].second = i;
}
sort(d.begin(), d.end(), greater<>());
for (int i = 0; i < n; ++i) {
if (i % 2) {
b[d[i].second]--;
a[d[i].second] = 0;
} else {
a[d[i].second]--;
b[d[i].second] = 0;
}
}
int ans = 0;
for (int i = 0; i < n; ++i) ans += a[i] - b[i];
cout << ans << endl;
}
}

F. Programming Competition

大致题意

一个公司,其上级或者间接的上级都是他老板,现在需要两两组队,但是有老板关系的不能组,问最多可以组几个队伍

思路

树上 dfs 搜索即可,每次传给当前节点,外面不属于他的员工的节点数量还有多少,有就用掉即可

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> p(n), d(n);
vector<vector<int>> tree(n);
for (int i = 1; i < n; ++i) {
int tmp;
cin >> tmp;
tree[tmp - 1].push_back(i);
}

function<int(int)> deep = [&](const int v) {
d[v] = 1;
for (auto& i: tree[v]) d[v] += deep(i);
return d[v];
};

int ans = 0;
function<void(int, int)> dfs = [&](const int v, int cnt) {
ans += cnt > 0;
if (cnt > 0) --cnt;

int sum = 0;
for (auto &i: tree[v]) sum += d[i];

for (auto &i: tree[v]) dfs(i, sum - d[i] + cnt);
};

// ReSharper disable once CppExpressionWithoutSideEffects
deep(0);
dfs(0, 0);
cout << ans / 2 << endl;
}
}

G2. Light Bulbs (Hard Version)

大致题意

这题还是挺有意思的

有一排灯,其中每种颜色的灯恰好有两个,可以选择开始的时候,点亮一部分灯,之后可以无限次进行如下操作

  • 点亮一盏灯,前提是和他相同颜色的另一盏灯已经点亮了
  • 选择一种颜色,其两盏灯都已经点亮了,将这两盏灯中间的所有灯点亮

问开始的时候最少选择几盏灯,就可以把所有灯都点亮。同时给出可以选择的数量

思路

首先,如果存在一个 $a$ 夹在两个 $b$ 之间,那么只需要点亮 $b$ 就可以点亮 $a$,我们可以进行建图来做

但是如果这样完整建图的成本会很高,所以得考虑优化一下
例如 $1,2,3,4,1,2,3,4$ 这种,就会变成一个全连接的图,而点数量有 $2 \times 10^5$,故不太行

由于这个图并不是传统的拓扑排序,而是只要上游任意一个点满足则满足(传统拓扑是上游均满足才满足)所以可以做反向路径压缩,即“别压缩”

例如上面的例子,仅实现与最后出现的那个区间进行连接,例如上图,则仅有 $1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 1$,
容易的得到这样建图也是可以满足要求的,因为不是传统拓扑

那么只需要在创建完成图之后,将拓扑的首个节点序列拿出来即可,这些节点是必选。因为有两个相同颜色的灯,所以是 $2^x$ 种选择

接下来考虑成环的情况,我们只需要找出最初试图进入环的那个节点(最左边的节点,因为它必然可以连接到剩下所有的节点)
看剩下环内的节点是否能够反向到达它即可。可以反向建图来做。至于最初进入环的节点,可以用并查集

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;

struct node {
int v, n;
};
stack<int> st;
vector<node> edge, redge;
vector head(n + 1, -1), rhead(n + 1, -1), deg(n + 1, 0), fa(n + 1, 0), vis(n + 1, 0);
edge.reserve(n);
redge.reserve(n);
for (int i = 0; i < fa.size(); ++i) fa[i] = i;
function<int(int)> finds = [&](int x) { return fa[x] == x ? x : fa[x] = finds(fa[x]); };
function join = [&](const int x, const int y) {
const int rx = finds(x), ry = finds(y);
if (rx == ry) return;
fa[rx] = ry;
};

for (int i = 0; i < n * 2; ++i) {
int tmp;
cin >> tmp;
while (!st.empty() && vis[st.top()] == 2) st.pop();
if (!st.empty() && st.top() != tmp) {
join(tmp, st.top());
edge.push_back({tmp, head[st.top()]});
head[st.top()] = static_cast<int>(edge.size()) - 1;
deg[tmp]++;

redge.push_back({st.top(), rhead[tmp]});
rhead[tmp] = static_cast<int>(redge.size()) - 1;
}
st.push(tmp);
++vis[tmp];
}

queue<int> q;
for (int i = 1; i <= n; ++i) if (deg[i] == 0) q.push(i);
constexpr long long mod = 998244353;
const int res = static_cast<int>(q.size());

long long ans = 1;
for (int i = 0; i < q.size(); ++i) {
ans <<= 1;
ans %= mod;
}

while (!q.empty()) {
const int cur = q.front();
q.pop();
if (vis[cur] == 3) continue;
++vis[cur];
for (int i = head[cur]; ~i; i = edge[i].n) q.push(edge[i].v);
}

map<int, int> superCnt;
for (int i = 1; i <= n; ++i) if (vis[i] != 3 && finds(i) == i) q.push(i);

while (!q.empty()) {
const int cur = q.front();
q.pop();
if (vis[cur] == 3) continue;
++vis[cur];
superCnt[finds(cur)]++;
for (int i = rhead[cur]; ~i; i = redge[i].n)
q.push(redge[i].v);
}

for (auto [k, v]: superCnt) ans = ans * 2 * v % mod;

cout << (res + superCnt.size()) << ' ' << ans << endl;
}
}
🔲 ☆

Codeforces Round 898 (Div. 4)

A. Short Sort

大致题意

有三张卡片,分别为 $a, b, c$,已经在桌面上乱序排好,最多交换两张卡片的位置,问是否能够变成有序的 $a, b, c$

思路

简单题,判断一下是不是至少有一位是保持 $a, b, c$ 的顺序即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
string str;
str.reserve(3);
cin >> str;
int cnt = 0;
for (int i = 0; i < 3; ++i) cnt += str[i] == i + 'a';
cout << (cnt == 1 || cnt == 3 ? "YES" : "NO") << endl;
}
}

B. Good Kid

大致题意

有一个数组,允许你给其中一个值加一,问最终所有值的乘积最大是多少

思路

简单题,如果有两个及以上的 $0$,那么最终结果一定还是 $0$。如果只有一个 $0$,那就等于忽略这个 $0$ 即可,剩下的情况,因为加了之后的效果是 $\frac{x+1}{x}$,所以 $x$ 越小越好,那么让最小的值 $+1$ 即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
int p = 1, zero = 0, mi = INT_MAX;
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
p *= (tmp == 0 ? 1 : tmp);
zero += tmp == 0;
mi = min(mi, tmp);
}

if (zero >= 2) cout << 0 << endl;
else if (zero == 1) cout << p << endl;
else cout << (p / mi * (mi + 1)) << endl;
}
}

C. Target Practice

大致题意

有一个飞镖靶,根据结果计算总分

思路

简单题,根据当前的下标距离四个边最小值是多少即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
string str;
str.reserve(10);
int ans = 0;
for (int i = 0; i < 10; ++i) {
cin >> str;
for (int j = 0; j < 10; ++j) {
if (str[j] == '.') continue;
int code = min(min(i + 1, 10 - i), min(j + 1, 10 - j));
ans += code;
}
}
cout << ans << endl;
}
}

D. 1D Eraser

大致题意

有一个串,其中有白色和黑色方块,每次可以选择连续 $k$ 个块让其变成白色,问最少几步可以全部变成白色

思路

简单题,从左往右考虑即可,毕竟最左边遇到的第一个黑色方块肯定需要消耗一次操作,为了最大化使用必定会让 $k$ 个的左边界是当前的黑色方块

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n >> k;
string str;
str.reserve(n);
cin >> str;
int last = -k - 1, ans = 0;
for (int i = 0; i < n; ++i) {
if (str[i] == 'W') continue;
if (i - last + 1 <= k) continue;
last = i;
ans++;
}
cout << ans << endl;
}
}

E. Building an Aquarium

大致题意

有一个线性水池,水池底部形状已知,最多可以使用 $x$ 个单位的水,问水池两边应该造多高才能尽可能容纳更多的水的同时,在水池满的时候不会使用超过给出的水

思路

简单题,二分答案即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, x;
cin >> n >> x;
vector<int> data(n);
for (auto &item: data) cin >> item;
int l = 0, r = 2e9 + 10LL;
while (l + 1 < r) {
int mid = (l + r) >> 1;
int sum = 0;
for (auto &item: data) sum += item >= mid ? 0 : mid - item;
if (sum > x) r = mid;
else l = mid;
}
cout << l << endl;
}
}

F. Money Trees

大致题意

给出一个数组,其每个值都有两个属性:$a, h$,需要找到一个连续的子数组,使得这个连续的子数组 $[l, r]$的 $h$ 值满足 $\forall i \in [l, r], h_{i-1} \space mod \space h_i = 0$,同时 $\sum_{i=l}^{r} a_i \leq x$

问最长的子数组的长度

思路

也是二分答案即可,毕竟在确定要找的最终串的长度的情况下,只需要 $O(n)$ 即可求出是否符合预期,注意平移区间的时候状态的转化

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k, ans = 0;
cin >> n >> k;
vector<int> a(n), h(n);
for (auto &item: a) cin >> item;
for (auto &item: h) cin >> item;
for (auto &item: a) if (item <= k) ans = 1;
int l = 0, r = n + 1;
while (l + 1 < r) {
int mid = (l + r) >> 1;
if (mid == 1) {
if (ans >= 1) l = mid;
else r = mid;
continue;
}
int x = 0, y = 0, sum = 0;
bool flag = false;

auto findNext = [&]() {
x = y;
y += 1;
sum = a[x];
while (y - x != mid && x < n && y < n) {
if (h[y - 1] % h[y] != 0) {
x = y;
y += 1;
sum = a[x];
} else {
sum += a[y];
y++;
}
}

if (y - x == mid && sum <= k) {
ans = max(ans, mid);
flag = true;
}
return y - x == mid;
};
auto move = [&]() {
if (y == n) return false;
if (h[y - 1] % h[y] != 0) return false;
sum -= a[x];
sum += a[y];
y++;
x++;
if (sum <= k) {
ans = max(ans, mid);
flag = true;
}
return true;
};

while (findNext()) while (move());
if (flag) l = mid;
else r = mid;
}

cout << ans << endl;
}
}

G. ABBC or BACB

大致题意

有一个字符串,由 $A, B$ 两个字母组成,每次可以将 $AB$ 转为 $BC$,或者将 $BA$ 转为 $CB$,问最多可以操作几次

思路

很显然,如果是一串联系的 $A$,然后其中一侧有一个 $B$,那么这种情况下的答案就是 $A$ 的数量

那么将整个数组拆成所有连续的 $A$ 段,然后为每个 $A$ 段找合理的 $B$ 即可。比如如果整个字符串开头或者结尾是 $B$,那么必然可以为每个 $A$ 段找到一个 $B$。除开上面的情况,只需要看看 $B 的数量是否大于等于 $A$ 段的数量即可,因为如果等于或者超过也必然可以分割。如果还不行,那么只能舍弃价值最低的 $A$ 串了

AC code

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
void solve() {
int _;
cin >> _;
string str;
str.reserve(2e5 + 10);
for (int ts = 0; ts < _; ++ts) {
cin >> str;
if (str.front() == 'B' || str.back() == 'B') {
int cnt = 0;
for (auto &item: str) cnt += item == 'A';
cout << cnt << endl;
continue;
}

vector<int> part(1, 0);
int cntB = 0;
for (char &i: str) {
cntB += i == 'B';
if (i == 'B') {
if (part.back() != 0) part.push_back(0);
} else part.back()++;
}

int tot = 0, mi = INT_MAX;
for (auto &item: part) {
tot += item;
mi = min(mi, item);
}
if (cntB < part.size()) tot -= mi;
cout << tot << endl;
}
}

H. Mad City

大致题意

有一个图,$n$ 个点,$n$ 条边,有两个人分别从 $a, b$ 出发,其中前者希望追赶后者,而后者希望摆脱前者的追捕,问能否追上

思路

首先,$n$ 个点和 $n$ 条边,那就意味着必然图中必然存在环。而两人速度相同,如果同时都在环上,那肯定追不到。所以必须要在后者进入环之前抓到,也就是提前或者刚好到达环上的某一个点。所以只需要找出后者刚进入环的时间点和位置,看看前者能否在指定时间内达到即可

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, a, b;
cin >> n >> a >> b;
vector<int> deg(n + 1, 0);
vector<bool> vis(n + 1, false);
struct node {
int v, n;
};
vector<node> edge(n * 2);
vector<int> head(n + 1, -1);
for (int i = 0; i < n; ++i) {
int u, v;
cin >> u >> v;
edge[i << 1] = {v, head[u]};
edge[(i << 1) | 1] = {u, head[v]};
head[u] = i << 1;
head[v] = (i << 1) | 1;
deg[u]++;
deg[v]++;
}

// find circle
queue<int> q;
for (int i = 1; i <= n; ++i) if (deg[i] == 1) q.push(i);
while (!q.empty()) {
int cur = q.front();
q.pop();
vis[cur] = true;
for (int i = head[cur]; i != -1; i = edge[i].n)
if ((--deg[edge[i].v]) == 1 && !vis[edge[i].v]) q.push(edge[i].v);
}

// begin for b run away
int cost1, cost2 = INT_MAX, target;
queue<pair<int, int>> qs;
vector<bool> flag(n + 1, false);
qs.emplace(b, 0);
flag[b] = true;
while (!qs.empty()) {
auto cur = qs.front();
qs.pop();
if (!vis[cur.first]) {
cost1 = cur.second;
target = cur.first;
break;
}
for (int i = head[cur.first]; i != -1; i = edge[i].n)
if (!flag[edge[i].v]) {
qs.emplace(edge[i].v, cur.second + 1);
flag[edge[i].v] = true;
}
}

while (!qs.empty()) qs.pop();
for (int i = 0; i <= n; ++i) flag[i] = false;

qs.emplace(a, 0);
flag[a] = true;
while (!qs.empty()) {
auto cur = qs.front();
qs.pop();
if (cur.first == target) {
cost2 = cur.second;
break;
}
for (int i = head[cur.first]; i != -1; i = edge[i].n)
if (!flag[edge[i].v]) {
qs.emplace(edge[i].v, cur.second + 1);
flag[edge[i].v] = true;
}
}

cout << (cost1 < cost2 ? "YES" : "NO") << endl;
}
}
🔲 ☆

CodeTON Round 6 (Div. 2)

A. MEXanized Array

大致题意

构造一个数组,其长度为 $n$,最大值为 $x$,$MEX$ 为 $k$,问这个数组的所有值的和最大是多少

思路

简单题,在 $k > x + 1$ 或 $n < k$ 的场景下无解(不可能构造一个 $MEX$ 不可达的数组),然后就随便构造就行了,保证 $MEX$ 之后,剩下所有值取最大就行

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k, x;
cin >> n >> k >> x;
if (k > x + 1 || n < k) {
cout << -1 << endl;
continue;
}
int sum = 0;
for (int i = 0; i < k; ++i) sum += i;
for (int i = k; i < n; ++i) sum += (x == k ? x - 1 : x);
cout << sum << endl;
}
}

B. Friendly Arrays

大致题意

给出两个数组 $a, b$,允许选择任意次的 $b$ 数组中的任意一个 $b_j$,然后让 $\forall i \in [1, len(a)], a_i = a_i | b_j$,问最终得到的数组 $a$ 中所有的异或和最大和最小的可能

思路

某个比特为是 $1$ 的情况下,在奇数个值异或和的结果则也是 $1$,而偶数个则为 $0$。而或运算可以让 $a$ 数组的每一个值的某些个位都变成 $1$。基于此,只需要关心 $a$ 的长度即可,若 $a$ 为奇数,那么选尽可能多的 $b$ 使得每个位都尽可能是 $1$,反之则尽可能不选,这样才能达到最大,同理可以得到最小的方案

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, m;
cin >> n >> m;
int sa = 0, sb = 0, tmp;
for (int i = 0; i < n; ++i) {
cin >> tmp;
sa ^= tmp;
}
for (int i = 0; i < m; ++i) {
cin >> tmp;
sb |= tmp;
}
cout << (n % 2 ? sa : sa & ~sb) << ' ' << (n % 2 ? sa | sb : sa) << endl;
}
}

C. Colorful Table

大致题意

有一个数组 $a$,长度为 $n$,然后有一个对应的矩阵 $b$,为 $n \times n$,其每一个位置的值 $b_{i,j}=min(a_i, a_j)$

问对于每个数字 $x$,在矩阵 $b$,中能够找到对应一个最小的矩形,此矩形包含了所有出现 $x$ 的位置,求出这个矩形的大小

思路

对于任意一个值,假定其第一次在 $a$ 中出现的位置为 $i$,它第一次出现在 $b$ 地点一定是 $b_{i,i}$,同时其最后一次在矩阵中的位置一定是 $b_{j,j}$,其中 $j$ 是在数组 $a$,中出现的,最后一个比当前值更大的下标

根据上面的规律,可以求出实际上每个值的位置,一定可以包裹比他大的那个值对应的矩阵,所以只需要根据值的大小排序一下他们在数组中第一次出现的位置,和最后一次出现的位置,然后从大到小遍历,保证小的值的区间能够覆盖到大的值的区间即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n >> k;
vector<bool> flag(k, false);
vector<pair<int, int>> data(k, {-1, -1});
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
data[tmp - 1].first == -1 ? data[tmp - 1].first = data[tmp - 1].second = i : data[tmp - 1].second = i;
flag[tmp - 1] = true;
}
for (int i = k - 2; i >= 0; --i) {
data[i].first = !flag[i] ? data[i + 1].first : (data[i + 1].first != -1 ? min(data[i].first, data[i + 1].first) : data[i].first);
data[i].second = !flag[i] ? data[i + 1].second : (data[i + 1].first != -1 ? max(data[i].second, data[i + 1].second) : data[i].second);
}
for (int i = 0; i < k; ++i)
cout << (!flag[i] ? 0 : (data[i].second - data[i].first + 1) + (data[i].second - data[i].first + 1)) << ' ';

cout << endl;
}
}

D. Prefix Purchase

大致题意

有一个初始数组,每一个值都是 $0$,每次你可以选择花费 $c_i$ 元,使得这个数组前 $i$ 个元素加一,最多只能花费 $k$ 元,问能够得到最大字典序的数组是什么

思路

首先需把 $c$ 的值进行单调递增栈处理一下,毕竟价格相同或更低的同时 $i$ 更大肯定有优势

回到题目中的字典序,意味着只有越前面的值越大即可,所以要尽可能满足最前面的值最大,所以直接把 $k$ 丢给处理后的第一个值,看看最多第一个值可以到多少

处理完成第一个值后,那就意味着后面无论怎么贪心,第一个值一定要达到这个,否则肯定不如现在更好。另外,对于字典序而言,约前面的值价值越高,所以要尽可能让前面的值大,贪心一下即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n;
vector<pair<int, int>> data;
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
while (!data.empty() && data.back().first >= tmp) data.pop_back();
data.emplace_back(tmp, i);
}
data.emplace_back(INT_MAX, n - 1);
cin >> k;
vector<int> ans(data.size());
ans[0] = k / data.front().first;
k %= data.front().first;
for (int i = 1; i < data.size(); ++i) {
int diff = data[i].first - data[i - 1].first;
if (k < diff) {
break;
}
ans[i] = min(ans[i - 1], k / diff);
k -= diff * ans[i];
}
int cur = 0;
for (int i = 0; i < n; ++i) {
while (data[cur].second < i) cur++;
cout << ans[cur] << " \n"[i == n - 1];
}
}
}
🔲 ☆

Codeforces Round 895 (Div. 3)

A. Two Vessels

大致题意

有 A,B 两个水池,用大小为 $c$ 的勺子舀,最少需要几次才能让这两个水池相同

思路

简单题,每次变化 $2c$,不要求平均数,不然精度不好算

AC code

1
2
3
4
5
6
7
8
9
10
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int a, b, c;
cin >> a >> b >> c;
int diff = abs(a - b);
cout << (diff + 2 * c - 1) / (2 * c) << endl;
}
}

B. The Corridor or There and Back Again

大致题意

有一条射线,射线上有一些点有夹子,夹子会在人经过后 $x$ 秒触发,问从顶点出发,然后折返,问最远可以到哪里

思路

也是简单题,每个夹子就意味着单独的最远可达距离,然后取最小就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
int ans = INT_MAX;
for (int i = 0; i < n; ++i) {
int a, b;
cin >> a >> b;
ans = min(ans, a + (b - 1) / 2);
}
cout << ans << endl;
}
}

C. Non-coprime Split

大致题意

给出 $[l, r]$ 这个区间,目的找到两个值 $a, b$,满足

  • $a + b \in [l, r]$
  • $gcd(a, b) \neq 1$

思路

第一反应就是偶数,毕竟任意两个偶数很容易达到,而且可以足够小,只要 $[l, r]$ 中存在偶数区间即可。当然,同时 $r \geq 4$,否则肯定没戏

如果还不行怎么办,那此时必然满足 $l = r, r \space mod \space 2 == 1$。这个时候,反正也只有一个数可以选了,强行找因子吧,找不到就算失败

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int l, r;
cin >> l >> r;
if (r < 4) {
cout << -1 << endl;
continue;
}
if (l != r || r % 2 == 0) {
cout << 2 << ' ' << ((r >> 1) << 1) - 2 << endl;
continue;
}
int mid = (int) sqrt(r) + 10;
int gcd = -1;
for (int i = 2; i <= mid; ++i) {
if (r - i <= 0) break;
if (r % i == 0) {
gcd = i;
break;
}
}
if (gcd == -1) {
cout << -1 << endl;
} else {
cout << gcd << ' ' << r - gcd << endl;
}
}
}

D. Plus Minus Permutation

大致题意

给定 $n, x, y$,你需要找到一个 $n$ 的排列,满足

$$((p_{1x}+p_{2x}+p_{3x}+ \dots + p_{\left \lfloor \frac{n}{x} \right \rfloor x}) - (p_{1y}+p_{2y}+p_{3y}+ \dots + p_{\left \lfloor \frac{n}{y} \right \rfloor y})$$

尽可能大,问最终结果是

思路

计算出 $x$ 单独占了几个,$y$ 单独占了几个,然后把大数给 $x$,小数给 $y$ 即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, x, y;
cin >> n >> x >> y;
int g = gcd(x, y), l = (x * y) / g;
int cntL = n / l, cntX = n / x - cntL, cntY = n / y - cntL;
int sumX = (n + (n - cntX + 1)) * cntX / 2, sumY = (1 + cntY) * cntY / 2;
cout << sumX - sumY << endl;
}
}

E. Data Structures Fan

大致题意

这题实在不想写题意了,已经把线段树这三个字拍脸上了

思路

维护两个值即可,为 $0$ 的异或和,为 $1$ 的异或和,然后每次改就是交换这两个值

AC code

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
const int N = 1e5 + 10;

struct SegTree {

int cnt[2][N << 1];
bool lazy[N << 1];

inline int static get(int l, int r) {
return (l + r) | (l != r);
}

inline void up(int l, int r) {
int mid = (l + r) >> 1;
cnt[0][get(l, r)] = cnt[0][get(l, mid)] ^ cnt[0][get(mid + 1, r)];
cnt[1][get(l, r)] = cnt[1][get(l, mid)] ^ cnt[1][get(mid + 1, r)];
}

void build(int l, int r) { // NOLINT(*-no-recursion)
lazy[get(l, r)] = false;
if (l == r) {
return;
}
int mid = (l + r) >> 1;
build(l, mid);
build(mid + 1, r);
up(l, r);
}

inline void push(int l, int r) {
int k = get(l, r);
if (lazy[k]) {
int mid = (l + r) >> 1;
int left = get(l, mid), right = get(mid + 1, r);
swap(cnt[0][left], cnt[1][left]);
swap(cnt[0][right], cnt[1][right]);
lazy[left] = !lazy[left];
lazy[right] = !lazy[right];
lazy[k] = false;
}
}

void update(int l, int r, int x, int y) { // NOLINT(*-no-recursion)
if (l == x && y == r) {
swap(cnt[0][get(l, r)], cnt[1][get(l, r)]);
lazy[get(l, r)] = !lazy[get(l, r)];
return;
}
push(l, r);
int mid = (l + r) >> 1;
if (y <= mid) {
update(l, mid, x, y);
} else if (x > mid) {
update(mid + 1, r, x, y);
} else {
update(l, mid, x, mid);
update(mid + 1, r, mid + 1, y);
}
up(l, r);
}

int query(int l, int r, int x, int y, int p) { // NOLINT(*-no-recursion)
if (l == x && y == r) {
return cnt[p][get(l, r)];
}
push(l, r);
int mid = (l + r) >> 1;
if (y <= mid) {
return query(l, mid, x, y, p);
} else if (x > mid) {
return query(mid + 1, r, x, y, p);
} else {
return query(l, mid, x, mid, p) ^ query(mid + 1, r, mid + 1, y, p);
}
}
} seg;

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> seg.cnt[0][SegTree::get(i, i)];
seg.cnt[1][SegTree::get(i, i)] = 0;
}
string str;
str.reserve(n);
cin >> str;
for (int i = 0; i < n; ++i)
if (str[i] == '1')
swap(seg.cnt[0][SegTree::get(i + 1, i + 1)], seg.cnt[1][SegTree::get(i + 1, i + 1)]);
seg.build(1, n);

int q;
cin >> q;
for (int i = 0; i < q; ++i) {
int o;
cin >> o;
if (o == 1) {
int l, r;
cin >> l >> r;
seg.update(1, n, l, r);
} else {
int x;
cin >> x;
cout << seg.query(1, n, 1, n, x) << ' ';
}
}
cout << endl;
}
}

F. Selling a Menagerie

大致题意

你决定逐个出售动物园里的动物,每个动物都有其价格,出售可以获得对应价格。

每个动物都有其唯一害怕的动物,如果你出售的时候,其害怕的动物还没有被出售,那么你可以获得双倍的价格奖励

给出一个出售顺序,使得最终价值最高

思路

很明显是一个 dag 的拓扑排序问题。可惜的是,这个并不是 dag,是有环的。而每次解开一个环,就要消耗一定代价。

可以直接将原来拓扑用的入度改成代价,即所有指向(害怕)这个节点的价格之和,因为如果这个节点先被拓扑了,那么这些指向它的节点就拿不到双倍的价值了,即损失了他们价格之和的代价

然后搞个优先队列拓扑就好了

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
struct node {
int v, n;
};

int n;
cin >> n;
vector<int> cost(n);
vector<int> link(n);
vector<int> deg(n, 0);
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
link[i] = tmp - 1;
}
for (auto &item: cost) cin >> item;
for (int i = 0; i < n; ++i) deg[link[i]] += cost[i];

vector<bool> visit(n, false);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> prq;
for (int i = 0; i < n; ++i) prq.emplace(deg[i], i);
while (!prq.empty()) {
auto cur = prq.top();
prq.pop();
if (visit[cur.second]) continue;
visit[cur.second] = true;
cout << cur.second + 1 << ' ';
int nxt = link[cur.second];
if (visit[nxt]) continue;
deg[nxt] -= cost[cur.second];
prq.emplace(deg[nxt], nxt);
}
cout << endl;
}
}

G. Replace With Product

大致题意

给你一个数组,允许你选择其中一段,将其换成这些值的乘积,问数组最大的和是多少。需要给出选择的那一段

思路

由于乘法的增长通常都会远大于求和,我们需要寻找一个临界点,使得 $\prod_{i=1}^n a_i \geq \sum_{i=1}^n a_i$,这样就可以无脑乘起来了

假定数组中只有两个值是 $> 1$ 的,为 $x, y$,那么可以得到

$$\begin{align*}xy & \geq n+x+y-2 \\ & \geq n+2\sqrt{xy}-2 \\let \space t & \rightarrow \sqrt{xy} \\t^2-2t+1 & \geq n - 1 \\t-1 & \geq \sqrt{n-1} \\t & \geq \sqrt{n-1}+1 \\xy & \geq n-1+2\sqrt{n-1}+1 \\ & \geq n+2\sqrt{n-1} \\ & \geq n+n-1+1 \\ & \geq 2n\end{align*}$$

所以只要对于 $xy > 2n$ 的情况,则可以无脑选尽可能全部的即可,因为相加一定不如相乘,当然,是尽可能,不是一定全部

那对于那些不满足的,可以得到 $\prod_{i=1}^n a_i < 2n$,这是一个很难达到的,假定有 $x$ 个数值不为 $1$ 的值,那么必然平均值为 $\sqrt[x]{2n}$,当 $x = 100$ 的时候,平均值就一定 $< 2$ 了,就意味着有 $1$ 的存在,那更不可能乘起来达到目标值了,所以此时非 $1$ 的值数量极少,可以暴力求解

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> data(n);
for (auto &item: data) cin >> item;
int tot = 1;
for (auto &item : data) {
tot *= item;
if (tot > 2 * n) break;
}

if (tot > 2 * n) {
// make 1 less
int l = 0, r = n - 1;
while (data[l] == 1) l++;
while (data[r] == 1) r--;
cout << l + 1 << ' ' << r + 1 << endl;
continue;
}

vector<int> not1;
for (int i = 0; i < n; ++i) if (data[i] != 1) not1.push_back(i);

if (not1.empty()) {
cout << 1 << ' ' << 1 << endl;
continue;
}

int mx = 0, l = 0, r = 0;
for (int i = 0; i < not1.size() - 1; ++i) {
int curP = data[not1[i]], curS = data[not1[i]] - 1;
for (int j = i + 1; j < not1.size(); ++j) {
curP *= data[not1[j]];
curS += data[not1[j]] - 1;
int realS = curS + not1[j] - not1[i] + 1;
if (mx < curP - realS) {
mx = curP - realS;
l = not1[i];
r = not1[j];
}
}
}
cout << l + 1 << ' ' << r + 1 << endl;
}
}
🔲 ☆

Codeforces Round 894 (Div. 3)

A. Gift Carpet

大致题意

从字符串矩阵中依次找出四列,满足依次包含 “vika” 四个字符

思路

简单题,不过多赘述

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, m;
cin >> n >> m;
vector<string> data(n);
for (auto &item : data) item.reserve(m);
for (auto &item : data) cin >> item;

string vika = "vika";
int cur = 0;
for (int i = 0; i < m && cur < vika.size(); ++i) {
for (int j = 0; j < n; ++j) {
if (data[j][i] == vika[cur]) {
cur++;
break;
}
}
}
cout << (cur == vika.size() ? "YES" : "NO") << endl;
}
}

B. Sequence Game

大致题意

有一个原始的序列,将其中的 $a_0$ 以及 $a_{i - 1} \leq a_i$ 的 $a_i$ 都提取出来给你,问可能的原始序列是什么

思路

简单题,如果提取后的某个值不满足上述条件的,在其前面加个 $1$ 就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> data(n);
for (int i = 0; i < n; ++i) cin >> data[i];

int add = 0;
for (int i = 1; i < n; ++i) add += data[i] < data[i - 1];
cout << n + add << endl;

cout << data[0];
for (int i = 1; i < n; ++i) {
if (data[i] < data[i - 1]) cout << ' ' << 1;
cout << ' ' << data[i];
}
cout << endl;
}
}

C. Flower City Fence

大致题意

判定将木板排序后,横着和竖着放是否完全相同

思路

简答题,第 $i$ 块木板的长度,是否恰好都等于 $\leq i$ 的模板数量

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> data(n);
for (int i = 0; i < n; ++i) cin >> data[i];
bool flag = true;

int ptr = n - 1;
for (int i = 0; i < n; ++i) {
while (ptr >= 0 && data[ptr] <= i) ptr--;
if (data[i] != ptr + 1) {
flag = false;
break;
}
}

cout << (flag ? "YES" : "NO") << endl;
}
}

D. Ice Cream Balls

大致题意

制作出恰好 $n$ 个不同的包含两个冰球的冰淇淋,需要多少个冰球(同时制作,两个冰淇淋之间不共用冰球)

思路

本题要求的恰好制作出,从最优方案上,肯定是不同的冰球更好,可以得到 $\frac{n \times (n - 1)}{2}$ 种冰淇淋,但是这样难以凑到恰好

通过上面的方案逼近答案后,再加一些重复的冰球,由于需要不同的冰淇淋,所以每种冰球的数量不能超过 $2$ 个,否则是溢出无意义的,不会带来更多方案

而每增加一个额外的重复冰球,仅能带来一种方案,即类似 ${1, 1}$ 这种重复冰球的方案。所以只需要一个简单的减法就行了

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, mid;
cin >> n;
mid = (int)sqrt(n * 2);
int ans = LONG_LONG_MAX;
for (int i = max(2LL, mid - 10); i < mid + 10; ++i) if (i * (i - 1) / 2 <= n)
ans = min(ans, i + n - (i * (i - 1) / 2));
cout << ans << endl;
}
}

E. Kolya and Movie Theatre

大致题意

在 $n$ 天内选出 $m$ 天,其中每一天能够拿到一定的分数,还需要扣除任意两个选出的天之间的分数差(默认选出第 0 天),分数差仅取决于天数差,问最大能拿到多少分

思路

这道题第一眼以为是需要 dp

但是仔细读题,会发现其实扣除的分数差就是最后选出的那一天的 $index$,因为恰好把所有区间加上了

那么就变得很简单了,只需要计算到达每天的位置,最大的 $m$ 个分数的值是哪些,用个堆就行了

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, m, d;
cin >> n >> m >> d;
int ans = 0, cur = 0;
priority_queue<int, vector<int>, greater<>> prq;
for (int i = 0; i < n; ++i) {
cur -= d;
int tmp;
cin >> tmp;
if (tmp < 0) continue;

if (prq.size() < m) {
prq.push(tmp);
cur += tmp;
} else if (tmp > prq.top()) {
cur -= prq.top();
cur += tmp;
prq.pop();
prq.push(tmp);
}
ans = max(ans, cur);
}

cout << ans << endl;
}
}

F. Magic Will Save the World

大致题意

有两种魔法,火魔法和水魔法,每种魔法每秒钟都会积攒对应的法力值,使用 $x$ 点法力值可以打败体力低于等于 $x$ 的怪,怪必须一次打死,问最多需要多少时间才能打死所有的怪

思路

题意中很容易看出是一个背包问题,类似均分为两堆,但是这里不是均分,而是有比例分,所以可以分别计算一次,避免出错

AC code

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
#define int long long

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int f, w;
cin >> f >> w;
int n, sum = 0;
cin >> n;
vector<int> data(n);
for (int i = 0; i < n; ++i) cin >> data[i];
for (int i = 0; i < n; ++i) sum += data[i];

if (f >= sum || w >= sum) {
cout << 1 << endl;
continue;
}

int p = (sum + f + w - 1) / (f + w), ans;
{
int target = f * p;
vector<int> dp(target + 1);
for (int i = 0; i < n; ++i)
for (int j = target; j >= data[i]; --j)
dp[j] = max(dp[j], dp[j - data[i]] + data[i]);

int maxDp = 0;
for (int i = 0; i <= target; ++i) maxDp = max(maxDp, dp[i]);
if (sum - maxDp <= p * w) ans = p;
else ans = (sum - maxDp + w - 1) / w;
dp.clear();
}

{
int target = w * p;
vector<int> dp(target + 1);
for (int i = 0; i < n; ++i)
for (int j = target; j >= data[i]; --j)
dp[j] = max(dp[j], dp[j - data[i]] + data[i]);

int maxDp = 0;
for (int i = 0; i <= target; ++i) maxDp = max(maxDp, dp[i]);
if (sum - maxDp <= p * f) ans = min(ans, p);
else ans = min(ans, (sum - maxDp + f - 1) / f);
dp.clear();
}

cout << ans << endl;

}
}

G. The Great Equalizer

大致题意

每次,将数组排序后,为一个数组中的每个值加上 $n, n - 1, n - 2 \dots, 1$,然后去重,重复,直到只剩下一个值,问最后这个值是什么。

不直接需要原数组的答案,是依次回答的,每次会修改数组中的值,然后询问,修改操作是继承的

思路

观察可以得到,最终结果实际上是 $max(a_i) - min(a_i) + max(a_i - a_{i-1}) + min(a_i)$,化简得到 $max(a_i) + max(a_i - a_{i-1})$。只需要维护好这两值即可

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n;
cin >> n;
vector<int> data(n);
for (auto &item : data) cin >> item;

vector<int> copy = data;
sort(copy.begin(), copy.end());

map<int, int> dif, cnt;
for (int i = 0; i < n; ++i) cnt[data[i]]++;
for (int i = 1; i < n; ++i) dif[copy[i] - copy[i - 1]]++;

int total = 0;
for (int i = 1; i < n; ++i) total += copy[i] - copy[i - 1];

int q;
cin >> q;
for (int i = 0; i < q; ++i) {
int index, x;
cin >> index >> x;
if (n == 1) {
data[0] = x;
cout << x << ' ';
continue;
}
int old = data[index - 1];

const auto oldIter = cnt.find(old);
int al, ar, am, bl, br, bm;
if (oldIter->second > 1) {
oldIter->second--;
bl = br = bm = 0;
} else {
int lv, rv;
if (oldIter == cnt.begin()) lv = -1;
else {
auto left = oldIter;
left--;
lv = left->first;
}
auto right = oldIter;
right++;
if (right == cnt.end()) rv = -1;
else rv = right->first;
bl = lv == -1 ? 0 : old - lv;
br = rv == -1 ? 0 : rv - old;
bm = lv == -1 || rv == -1 ? 0 : bl + br;
cnt.erase(oldIter);
}
data[index - 1] = x;

const auto newIter = cnt.upper_bound(x);
if (newIter == cnt.end()) {
ar = am = 0;
auto iter = newIter;
iter--;
al = x - iter->first;
} else if (newIter == cnt.begin()) {
al = am = 0;
ar = newIter->first - x;
} else {
int lv, rv = newIter->first;
auto tmp = newIter;
--tmp;
lv = tmp->first;
al = x - lv;
ar = rv - x;
am = rv - lv;
}
cnt[x]++;

auto del = [&](int t) {
auto iter = dif.find(t);
if (iter == dif.end()) return;
if (iter->second == 1) dif.erase(iter);
else iter->second--;
};

dif[al]++;
dif[ar]++;
dif[bm]++;
del(bl);
del(br);
del(am);

cout << cnt.rbegin()->first + dif.rbegin()->first << ' ';
}
cout << endl;
}
}
🔲 ☆

Educational Codeforces Round#153 (Div. 2)

A. Not a Substring

大致题意

需要构建一个只有小括号构成的字符串,既满足括号匹配,同时不存在一个子串等同于给出的一个字符串

思路

实际上很简单,只需要取 ()()()() 模式 和 (((()))) 这两种即可,因为这两种模式的唯一相同的子串就只有一对 (),而若需要一个满足括号匹配的字符串,那么必然存在 (),故这两种模式就可以应对所有情况

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
string str, ans;
cin >> str;
ans.resize(str.size() << 1);
for (int i = 0; i < str.size(); ++i) ans[i] = '(';
for (int i = 0; i < str.size(); ++i) ans[i + str.size()] = ')';
if (strstr(ans.c_str(), str.c_str()) == nullptr) {
cout << "YES" << endl;
cout << ans << endl;
continue;
}
for (int i = 0; i < str.size(); ++i) ans[i * 2] = '(';
for (int i = 0; i < str.size(); ++i) ans[i * 2 + 1] = ')';
if (strstr(ans.c_str(), str.c_str()) == nullptr) {
cout << "YES" << endl;
cout << ans << endl;
continue;
}
cout << "NO" << endl;
}
}

B. Fancy Coins

大致题意

有 $a1$ 个 $1$ 元,$a2$ 个 $k$ 元,同时你可以“借来”无限量的 $1$ 元和 $k$ 元,问组成 $m$ 元最多需要借多少硬币

思路

简单卡一下边界,多一个 $k$ 元和少一个 $k$ 元的两种情况考虑一下即可,比较简单

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int m, k, a1, a2;
cin >> m >> k >> a2 >> a1;
m -= min(m / k, a1) * k;
if (m <= a2) {
cout << 0 << endl;
continue;
}

int ls = (m - a2) / k;
int ans = ls + (m - a2 - ls * k);
if (m - (ls + 1) * k >= 0) ans = min(ans, ls + 1);
cout << ans << endl;
}
}

C. Game on Permutation

大致题意

有一个数组,开始位置可以是任意的一个下标,每次可以移动到当前位置左边的任意一个值小于当前的位置。

两个人依次操作,谁最后无法进行操作了,谁胜利,问放在哪些位置,可以保证第二个开始操作的胜利

思路

假如说我摆放在一个位置,然后可以通过 $3$ 个依次操作达到最终无法移动(例如 $a \rightarrow b \rightarrow c \rightarrow d$),那么此时应该说第二个移动的人胜利

但是这个操作是可跳过的,因为你可以移动 $3$ 次,那么就必然可以一次移动到底,因为一定也符合题意,那么第一个移动的人为什么要遵循一个个移动呢,他完全可以直接 $a \rightarrow c$,然后第二个操作的人只能移动到 $d$,然后输了游戏

所以必须卡在一些只能移动一次的地方,否则就有可乘之机。

那么就必须保证选择的点满足

  • 大于左边最小的值
  • 小于左边之前确认的满足条件的点

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, ans = 0, curMin = INT_MAX, curMax = INT_MAX;
cin >> n;
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
if (tmp < curMin) curMin = tmp;
else if (tmp > curMin && tmp < curMax) {
ans++;
curMax = tmp;
}
}
cout << ans << endl;
}
}
🔲 ☆

Codeforces Round 890 (Div. 2)

A. Tales of a Sort

大致题意

有个数列,每次操作可以将所有值减少 $1$,除非它已经是 $0$ 了,问需要多少次操作,才能将整个数列变成非递减数列

思路

很明显的一点,如果发现一对不满足条件的相邻对,即 $a_i > a_{i + 1}$,如果不把他们都减少到 $0$ 的情况下,永远无法满足题目条件,故只需要找到不满足的对,然后取最大的那个值即可

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, last = 0, mx = 0, tmp;
bool ans = false;
cin >> n;
for (int i = 0; i < n; ++i) {
cin >> tmp;
if (tmp < last) mx = max(mx, last);
else last = tmp;
}
cout << mx << endl;
}
}

B. Good Arrays

大致题意

题目给出一个数组 $a$,要你判定是否存在另外一个数组 $b$,满足 $\sum_{i=1}^n a_i = \sum_{i=1}^n b_i, \forall i \in [1, n], a_i \neq b_i, a_i > 0, b_i > 0$

思路

读题易得:若原来的值是 $1$,那么必须找别的值借 $1$ 才能保证 $a_i \neq b_i$,而其他值则都可以简单变成 $1$ 解决。故只需要计算有多少可以冗余调配的值即可

需要特判一下只有一个值的情况

AC code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, cnt = 0, sum = 0;
cin >> n;
for (int i = 0; i < n; ++i) {
int tmp;
cin >> tmp;
cnt += tmp == 1;
sum += tmp;
}
cout << (cnt + n > sum || n == 1 ? "NO" : "YES") << endl;
}
}

C. To Become Max

大致题意

有一个初始数组 $a$,每次可以选择一个 $i \in [1, n - 1]$,若 $a[i] \leq a[i + 1]$ 则使得 $a[i] = a[i] + 1$,问在最多操作 $k$ 次的情况下,数组的最大值可以为多少

思路

注意到数据量,$n$ 仅有 $1000$,意味着复杂度可以达到 $n^2 log(1e9)$ 的级别,然后再做思考

按照公式,那么最终得到的数组,必定存在一个恰好递减的阶梯。另外很明显的是,数组的最后一个值必定不可动,那就意味着实际上最大值的可行性是被最后一个值限定的,最大值为 $a_n + n - 1$

由于复杂度有非常大的冗余,故可以作出如下的暴力搜索

  • 遍历所有可能为最大值的下标 $i$
  • 二分查找最终的最大值
  • 尝试构建最大值的可能性,是否能够在 $k$ 消耗内,完成构建一个阶梯

这样下来恰好复杂度满足预期

AC code

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
void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
int n, k;
cin >> n >> k;
vector<int> data(n);
for (int i = 0; i < n; ++i) cin >> data[i];
int ans = 0;
for (int i = 0; i < n; ++i) ans = max(ans, data[i]);

for (int l = 0; l < n; ++l) {
int b = ans, e = ans + k + 1;
while (b + 1 < e) {
int mid = (b + e) / 2;
int cost = 0;
bool keyPoint = false;
for (int i = l; i < n && !keyPoint && cost <= k; ++i) {
if (data[i] >= mid - (i - l)) keyPoint = true;
else cost += mid - data[i] - (i - l);
}
if (keyPoint && cost <= k) b = mid;
else e = mid;
}
ans = max(ans, b);
}

cout << ans << endl;
}
}

D. More Wrong

大致题意

交互题

有一个 $n$ 的排列 $a$,只知道长度 $n$,每次可以询问 $[l, r]$ 区间下,逆序对数量,每次询问的代价是 $(r - l)^2$,问如何在 $5 \times n^2$ 的代价下,找到最大值的下标

思路

区间逆序对,很容易想到归并排序

先得到两个简单的结论:

  • 若已知一个区间的逆序对数量,再最后加入一个元素,且没有改变逆序对数量,那么这个加入的元素必定是当前区间的最大值
  • 若已知一个区间的逆序对数量,再最前面加入一个元素,且增加了恰好等于原来元素个数的逆序对,则新加入的元素必定是当前区间的最大值

这两个结论显而易见,就不再解释

从归并排序的视角看,我们假定找到了一个区间前半部分的最大值的下标,又找到了后半部分最大值的下标,那么需要判断这两个值谁更大的时候,就可以通过上面的定律来判定,只需要两次查询即可

如此递归下去,可以得到最终的查询费用为

$$\begin{cases}& 2(n - 1)^2 + 2 \times 2(0.5n - 1)^2 + 2 \times 4(0.25n - 1)^2 + \dots \\\leq & 2n^2 + 4(0.5n)^2 + 8(0.25n)^2 + \dots \\= & 2(n^2 + 0.5n^2 + 0.25n^2 + \dots) \leq 4n^2 \leq 5n^2\end{cases}$$

可以证得这个方法的消耗低于要求

AC code

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
map<pair<int, int>, int> m;

int interactive(int l, int r) {
auto iter = m.find({l, r});
if (iter != m.end()) {
return iter->second;
}
int temp;
cout << "? " << l << ' ' << r << endl;
cout.flush();
cin >> temp;
m.insert({{l, r}, temp});
return temp;
}

int dfs(int l, int r) {
if (l == r) {
return l;
}
if (l + 1 == r) {
return interactive(l, r) == 1 ? l : r;
}
if (l + 2 == r) {
int lm = dfs(l, l + 1);
if (lm == l) {
return interactive(l, r) == 1 ? r : l;
} else return dfs(lm, r);
}

int mid = (l + r) >> 1;
int lm = dfs(l, mid);
int rm = dfs(mid + 1, r);
if (lm + 2 >= rm) return dfs(lm, rm);

int t1 = interactive(lm + 1, rm - 1);
int t2 = interactive(lm, rm);
return t2 >= t1 + rm - lm ? lm : rm;

}

void solve() {
int _;
cin >> _;
for (int ts = 0; ts < _; ++ts) {
m.clear();
int n;
cin >> n;
int ans = dfs(1, n);
cout << "! " << ans << endl;
}
}

E1. PermuTree (easy version)

大致题意

有一个树和一个 $n$ 的排列 $a$,求出使得满足 $a_u < a_{lca(u, v)} < a_v$ 这个等式的最多的排列下,满足多少次

思路

在树上并没有 $u, v$ 之分,实际上可以相互对掉,所以这棵树实际上需要尽可能满足二叉搜索树的结构才行,即每个节点下,左边的值都小于当前节点,右边的值都大于当前节点。

但是这不一定是一颗二叉树,而是多叉树,而在满足上述等式的情况下,则需要人为的将所有子节点划分为两份,一份大于一份小于。即,假如一个节点有 $3$ 个直接子节点,那么必定存在有两个直接的子节点的下的所有值都小于当前节点,同时另外一个直接子节点下的所有值都要大于此节点,那么最终的满足等式的量级为 $(cnt_1 + cnt_2) \times cnt_3$

而又因为划分的时候,总共的子节点数量之和是确定的,故需要尽可能对半分,那么就需要背包运算

而这又是树结构,所以只需要在树上 dp 上做背包 dp 即可

AC code

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
struct node {
int v, nxt;
};

vector<node> edge(5010);
vector<int> head(5010);

int dp(vector<int> &pack) {
if (pack.size() == 0) {
return 0;
}
if (pack.size() == 1) {
return 0;
}
if (pack.size() == 2) {
return pack[0] * pack[1];
}
int sum = 0;
for (int i : pack) sum += i;

vector<int> dp(sum / 2 + 1, 0);
for (int i : pack)
for (int w = sum / 2; w >= i; --w)
dp[w] = max(dp[w], dp[w - i] + i);

int left = 0;
for (auto i : dp) left = max(left, i);
return left * (sum - left);
}

int tree(int index, int &cal) {
int res = 1;
vector<int> temp;
for (int i = head[index]; i != -1; i = edge[i].nxt) {
temp.push_back(tree(edge[i].v, cal));
}
cal += dp(temp);
for (int i : temp) res += i;
return res;
}

void solve() {
int n;
cin >> n;

for (int i = 0; i <= n; ++i) head[i] = -1;
for (int i = 1; i < n; ++i) {
int tmp;
cin >> tmp;
edge[i] = {i + 1, head[tmp]};
head[tmp] = i;
}

int ans = 0;
tree(1, ans);
cout << ans << endl;
}
🔲 ☆

Java Script 的 null 和 undefined 随想

有些时候感觉一些语言里看起来很蠢的设计,实际上却能解决一些很有意思的场景。比如 JavaScript 的 null 和 undefined,虽然看起来都是表示空的意思,但是实际上却解决了“没有这个值”,“这个值为空”这样两种语义。在缓存穿透的问题上,如果 redis、memcached 等数据库也有这样一层设计等话,是不是就能解决 null 穿透问题了呢

❌