Codeforces Round 1003 (Div. 4) - 题解
A - Skibidus and Amog’u
模拟
如题意,输出前
n
−
2
n-2
n−2 个字符 + i 即可。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
string str;
cin >> str;
cout << str.substr(0, str.size() - 2) << "i\n";
}
}
B - Skibidus and Ohio
思维
我称之为 “点燃” 类,出题思路常用于简单题。一旦 出现相邻两个字符串相同,其中一个被删除,字符串长度 − 1 -1 −1,另一个变为左侧或右侧字符,则必然与该侧字符一起再次达成 “相邻两个字符串相同” 的条件,循环往复,直至只剩一个字符。
判断字符串中是否存在相邻两字符相同,若存在则输出 1 1 1,反之为字符串长度。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
string str;
cin >> str;
int res = str.length();
for (int i = 1; i < res; i++)
if (str[i] == str[i - 1]) res = 1;
cout << res << '\n';
}
}
C - Skibidus and Fanum Tax
贪心,排序,二分
从左往右考虑,当前数变得越小,则后续 “选择余地” 只会越大,文绉绉地说,这叫贪心的 “决策包容性”。故我们依次考虑每个 a i a_i ai 选择让其尽可能小。
对于
a
i
a_i
ai,若进行操作,则变为
b
j
−
a
i
b_j-a_i
bj−ai。由于限制 + 贪心策略,需找出
b
j
−
a
i
≥
a
i
−
1
b_j-a_i\geq a_{i-1}
bj−ai≥ai−1 下最小的
b
j
−
a
i
b_j-a_i
bj−ai,即找出
b
j
≥
a
i
−
1
+
a
i
b_j\geq a_{i-1}+a_i
bj≥ai−1+ai 下最小的
b
j
b_j
bj. 将数组 b 排序,再二分搜索即可,可使用 lower_bound.
对于可行(即
≥
a
i
−
1
\geq a_{i-1}
≥ai−1的)的
a
i
a_i
ai 与
b
j
−
a
i
b_j-a_i
bj−ai,选择较小者作为更新后的
a
i
a_i
ai,若二者均不可行(即都
<
a
i
−
1
<a_{i-1}
<ai−1),则无法构成,输出 NO。反之构造完毕,则输出 YES。
- 时间复杂度: O ( m + n log m ) O(m+n\log{m}) O(m+nlogm)
- 空间复杂度: O ( n + m ) O(n+m) O(n+m)
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5;
int a[N], b[N];
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
int n, m, last = INT_MIN + 1, res = 1;
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < m; i++) cin >> b[i];
sort(b, b + m);
for (int i = 0; i < n; i++) {
int tar = lower_bound(b, b + m, a[i] + last) - b;
if (tar == m) {
if (a[i] < last) res = 0;
last = a[i];
} else {
if (a[i] < last) last = b[tar] - a[i];
else last = min(a[i], b[tar] - a[i]);
}
}
cout << (res ? "YES\n" : "NO\n");
}
}
D - Skibidus and Sigma
搜索
这是一个经典贪心策略的弱化版(一开始我误理解为下面的改造版)。
其答案为 ∑ i = 1 n ( n − i + 1 ) × a i = n a 1 + ( n − 1 ) a 2 + ⋯ + a n \sum_{i=1}^n (n-i+1)\times a_i=na_1+(n-1)a_2+\cdots+a_n ∑i=1n(n−i+1)×ai=na1+(n−1)a2+⋯+an,用 v i v_i vi 表示第 i i i 个数组, s i s_i si 表示 v i v_i vi 的和。若数组向左移动 1 1 1 单位,则该数组每个元素系数 + 1 +1 +1,整体和 + s i +s_i +si,反之向右移动 1 1 1 单位,整体和 − s i -s_i −si.
考虑数组序列 ⋯ v i v i + 1 ⋯ \cdots v_iv_{i+1}\cdots ⋯vivi+1⋯,若交换 v i v i + 1 v_iv_{i+1} vivi+1,因每个数组长度为 m m m ,则对答案的贡献为 m s i + 1 − m s i = m ( s i + 1 − s i ) ms_{i+1}-ms_i=m(s_{i+1}-s_i) msi+1−msi=m(si+1−si),故数组和越大,则优先级越高,按数组和降序即可。
- 时间复杂度: O ( n m log m ) O(nm\log{m}) O(nmlogm)
- 空间复杂度: O ( n m ) O(nm) O(nm)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
LL n, m;
cin >> n >> m;
vector<pair<LL, vector<LL>>> arr(n, { 0, vector<LL>(m)});
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
cin >> arr[i].second[j];
arr[i].first = accumulate(arr[i].second.begin(), arr[i].second.end(), 0LL);
}
sort(arr.rbegin(), arr.rend());
LL idx = n * m, res = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
res += arr[i].second[j] * idx--;
cout << res << '\n';
}
}
我们对问题加以改造,若每个数组长度并不定长呢?同样考虑数组序列 ⋯ v i v i + 1 ⋯ \cdots v_iv_{i+1}\cdots ⋯vivi+1⋯,其 s i , s i + 1 s_i,s_{i+1} si,si+1 表示二者的数组和, l i , l i + 1 l_i,l_{i+1} li,li+1 表示二者数组长度,若交换 v i vi vi 与 v i + 1 v_{i+1} vi+1 的位置,则对答案的影响为 l i s i + 1 − l i + 1 s i l_is_{i+1}-l_{i+1}s_i lisi+1−li+1si,排序时根据该值进行比较即可。也可进一步变换,若 l i s i + 1 − l i + 1 s i > 0 l_is_{i+1}-l_{i+1}s_i>0 lisi+1−li+1si>0 则交换,也即 s i l i > s i + 1 l i + 1 \frac{s_i}{l_i}>\frac{s_{i+1}}{l_{i+1}} lisi>li+1si+1,即根据数组的平均值排序即可。
将第 19 19 19 行代码改为:
sort(arr.rbegin(), arr.rend(), [](const pair<LL, vector<LL>> &a, const pair<LL, vector<LL>> &b) { return a.first * b.second.size() < a.second.size() * b.first; });对输入输出的双重
for循环略作修改即可。
基本版:P1223 排队接水
相同思想的题目仍有很多,该类题目答案情形为序列(数组)形式,交换其中相邻两项,并不会其他部分的 “贡献”(对答案的计算)产生影响,则在该局部(这两项中)采取对答案贡献最大的排位。
看似,若选择序列中任意两项(中间跨越若干项),则可能对其余部分产生影响(如 P1223 中),使得难以比较;同样,比较的性质也不一定像本题的改造版这样直观,可以归约到只与自身有关(数组本身的平均值),不能直接推广至全局。但为啥只要实现相邻两项比较加上 sort 就能得到全局最优?
因为其还有一性质 —— 偏序性。
建议百度以获得严格的定义与理解。
给一集合 S S S,任选两个元素 a , b ( a ≠ b ) a,b(a\neq b) a,b(a=b) ,你总有一个函数 f ( a , b ) f(a,b) f(a,b),或者说法则、方法等便于理解的称呼,使得输出 a a a 和 b b b 哪一个 “更重要” 或类似的一种指向 关系。
- 若 a a a 比 b b b “更重要”,则 b b b 不可能比 a a a “更重要”,即这个谁比谁更重要的关系是单向的,即 非对称性 的。(看似是废话,因为便于理解,选用 “更重要” 该词作为关系的名称,此词的语义即包含了偏序的意味)
- 若 a a a 比 b b b “更重要”, b b b 比 c c c “更重要”,则 a a a 比 c c c “更重要”,即 传递性。
- 自己不比自己 “更重要”,即 “反自反性”。
满足这三个条件关系的即为 严格偏序关系。
如整数集合上的 大于 关系、小于 关系则是严格偏序关系。
对上述条件加以改造,则可得到 非严格偏序关系,如整数集合上的 大于等于 关系、小于等于 关系。
对于集合
S
S
S 而言,想象中,我们可以根据 偏序关系 将其排位一列,“更重要的” 在前,更不重要的在后(我也不知道你想象中的前后是哪)。而 sort 或者说排序算法,则可对集合中的若干元素,按照 偏序关系 排好。如普通的 sort 可以将实数数组,按 小于 关系升序排列。
如果理解有困难,则可以把抽象集合
S
S
S 中的每个元素,标以(映射到)一个实数,上面那个 “更重要” 的关系想象成 小于 关系,那 sort 显然可以把这玩意排好序。
回到题目,上述比较方法,实际是一个 非严格偏序关系,证明下对任意数组构成的集合
S
S
S,该比较方法,满足 自反性、反对称性、传递性(此为 非严格偏序关系 的三性)即可使用 sort 排序。
赛时怎么办?我们一般不证,直接用。当然此处我也没有证明, 非严格偏序关系 只是猜测。
想象下,给
sort传入一破坏了传递性的比较函数,可能发生什么?
E - Skibidus and Rizz
构造
即任意一子串,其 0 0 0 个数与 1 1 1 个数之差即为平衡值。构造一 0 / 1 0/1 0/1 串,使所有子串的最大平衡值 恰为 k k k(每个子串的平衡值 ≤ k \leq k ≤k,同时有至少一个子串平衡值为 k k k)。
若 m a x { n , m } < k max\{n,m\}<k max{n,m}<k,因平衡值最大只能为 m a x { n , m } max\{n,m\} max{n,m},输出 − 1 -1 −1.
若 ∣ n − m ∣ > k |n-m|>k ∣n−m∣>k,因整个 0 / 1 0/1 0/1 串的平衡值为 ∣ n − m ∣ |n-m| ∣n−m∣,输出 − 1 -1 −1.
排除上述两条件,其余情况均可构造,考虑 k k k 个 0 0 0, k k k 个 1 1 1,不足则全部输出。证明略。
- 时间复杂度: O ( n + m ) O(n+m) O(n+m)
- 空间复杂度: O ( n + m ) O(n+m) O(n+m)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
int n, m, k;
cin >> n >> m >> k;
if (abs(n - m) > k || max(n, m) < k) cout << "-1\n";
else {
if (m > n) {
cout << string(min(m, k),'1');
m -= k;
}
while (n > 0) {
cout << string(min(n, k),'0');
if (m > 0) cout << string(min(m, k),'1');
n -= k;
m -= k;
}
cout << '\n';
}
}
}
F - Skibidus and Slay
图论
非平凡简单路径(non-trivial simple path),非平凡(non-trivial)指路径中点数不为 1 1 1。
给一条长为 n ≥ 2 n\geq2 n≥2的(非平凡)简单路径,假定其 多数 为 k k k.
若其中出现两相邻点,值均为 k k k,则在考虑由这两点加之其连边构成的长为 2 2 2 的简单路径时,即可得到一 多数 k k k.
若其中并未出现两相邻元素均为 k k k,则 k k k 必定在数组中交替,形如以下:
n n n 为奇: k , ? , k , ⋯ , ? , k k,?,k,\cdots,?,k k,?,k,⋯,?,k,
n n n 为偶: k , ? , k , ⋯ , k , ? k,?,k,\cdots,k,? k,?,k,⋯,k,?(其翻转形式同)
我们只需考虑由该路径前 3 3 3 个点及其 2 2 2 条连边的子路径(或者说一定存在这样一条子路径),则可得到一 多数 k k k.
由上,只需考虑长度为 2 2 2 与 3 3 3 的路径,即可将所有简单路径产生的 多数 考虑 完备。
更进一步,我们只需考虑对每个点,及其周围一圈的所有点(构成菊花)的数计数,统计出现次数 ≥ 2 \geq2 ≥2 的数,即等价考虑长度为 2 2 2 与 3 3 3 的路径的 多数。
记某点为中心点,其周围一圈(仅经过一条路径到达)的点记为边缘点。
在某一中心点及其所有边缘点构成的图中,若 k k k 出现次数 ≥ 2 \geq2 ≥2,则至少构成如下情形:
- 中心点与一边缘点均为 k k k,出现上述情形 1 1 1
- 两边缘点为 k k k,则这两点通过中心点连接,出现上述情形 2 2 2
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 1, M = 2 * N;
int arr[N];
int head[N], val[M] , linked[M], last = 1;
bool res[N];
void add(int a, int b) {
linked[last] = head[a];
val[last] = b;
head[a] = last++;
}
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
last = 1;
for (int i = 1; i <= n; i++) {
res[i] = false;
head[i] = 0;
cin >> arr[i];
}
for (int i = 1; i < n; i++) {
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
for (int v = 1; v <= n; v++) {
map<int, int> cnt;
cnt[arr[v]]++;
for (int i = head[v]; i; i = linked[i])
if (++cnt[arr[val[i]]] >= 2) res[arr[val[i]]] = true;
}
for (int i = 1; i <= n; i++)
cout << (res[i] ? "1" : "0");
cout << '\n';
}
}
G - Skibidus and Capping
数论
从左到右依次考虑:
-
若 a i a_i ai 为素数 p p p,则可与左侧带有 p p p 的半素数 p q pq pq 构成 l c m ( p q , p ) = p q lcm(pq,p)=pq lcm(pq,p)=pq,也可与不为 p p p 的素数 q q q 构成 l c m ( q , p ) = p q lcm(q,p)=pq lcm(q,p)=pq
-
若 a i a_i ai 为素数 p p p 的平方 p 2 p^2 p2,则可与左侧的 p p p 与 p 2 p^2 p2 构成 l c m ( p , p 2 ) = l c m ( p 2 , p 2 ) = p 2 lcm(p,p^2)=lcm(p^2,p^2)=p^2 lcm(p,p2)=lcm(p2,p2)=p2
-
若 a i a_i ai 为两素数 p , q p,q p,q 乘积 p q pq pq,则可与左侧的 p p p、 q q q 与 p q pq pq 构成 l c m ( p , p q ) = l c m ( q , p q ) = l c m ( p q , p q ) = p q lcm(p,pq)=lcm(q,pq)=lcm(pq,pq)=pq lcm(p,pq)=lcm(q,pq)=lcm(pq,pq)=pq
综上,先筛出 1 ∼ 2 × 1 0 5 1\sim2\times10^5 1∼2×105 内的素数,然后维护每个数出现次数、素数出现次数、含每个因子的半素数出现次数即可。
-
时间复杂度: O ( n log n ) O(n\log{n}) O(nlogn)
-
空间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 1;
int factor[N], prime[N], cnt = 0;
void init(int n) {
for (int i = 2; i <= n; ++i) {
if (!factor[i]) factor[i] = prime[cnt++] = i;
for (int j = 0; prime[j] <= factor[i]; j++) {
if ((LL)i * prime[j] > n) break;
factor[i * prime[j]] = prime[j];
if (i % prime[j] == 0) break;
}
}
}
int main() {
cin.tie(NULL)->sync_with_stdio(false);
init(N - 1);
int t;
cin >> t;
while (t--) {
LL n, res = 0, cp = 0;
cin >> n;
map<LL,LL> cnt, fac;
for (int i = 0; i < n; i++) {
LL x;
cin >> x;
cnt[x]++;
LL p = factor[x], q = x / factor[x];
if (q == 1) {
cp++;
res += fac[p] + cp - cnt[p];
} else if (p == q) {
res += cnt[p] + cnt[p * p];
fac[p]++;
} else if (factor[q] == q) {
res += cnt[p] + cnt[q] + cnt[x];
fac[q]++;
fac[p]++;
}
}
cout << res << '\n';
}
}
H - Bro Thinks He’s Him
树状数组
这是一道典型的算 贡献 的问题,我们把
f
(
x
)
f(x)
f(x) 称之为
01
01
01 串
x
x
x 的 贡献。其定义值串中连续
0
0
0 或连续
1
1
1 的段的数量,可以进一步理解为为串中
0
/
1
0/1
0/1 交替次数
+
1
+1
+1,交替次数即 01 与 10 出现次数。
我们先看由 0 0 0 变 1 1 1 的情况,由其对称性, 1 1 1 变 0 0 0 的情况易得。
微观上,一
01
01
01 串某位发生反转,对其贡献的影响只需看该位与相邻位的情况即可(由第一段,其贡献可以视为统计01 与 10 出现次数
+
1
+1
+1,某一位的改变,显然不会影响更远位置的情况),我们枚举所有情况,观察对每种序列贡献的影响。
比如 ⋯ 0 0 ‾ \cdots 0\underline{0} ⋯00 的情况(反转位为序列尾,左侧为 0 0 0),此时该种序列的贡献 + 1 +1 +1;再如 ⋯ 1 0 ‾ 1 ⋯ \cdots1\underline{0}1\cdots ⋯101⋯(反转位左右两侧均为 1 1 1),此时该种序列的贡献 + 2 +2 +2。如此,反转位左右两侧可能为 0 0 0、 1 1 1、无,枚举总共有 3 × 3 = 9 3\times3=9 3×3=9 种情况,见下表。
宏观上,我们将 每种序列的贡献影响 与 该种序列数量 相乘即可得到对全局贡献的影响(好似一句废话)。 ⋯ 0 0 ‾ \cdots 0\underline{0} ⋯00 的情况中,该类型序列数量为 反转位左侧以 0 0 0 结尾的序列数; ⋯ 1 0 ‾ 1 ⋯ \cdots1\underline{0}1\cdots ⋯101⋯ 则是 反转位左侧以 1 1 1 结尾的序列数 × 反转位右侧以 1 1 1 开始的序列数。
因为具体到某种序列中,某侧位无的情况相当于 1 1 1。故我们只需统计左侧以 0 / 1 0/1 0/1 结尾的序列数,以及右侧以 0 / 1 0/1 0/1 开头的序列数,共 4 4 4 种,再组合下即可。
给一 01 01 01 串,其第 i i i 位左侧以 0 0 0 结尾的序列个数为:考虑 i i i 左侧所有出现 0 0 0 的位置,假设第 j j j 处为 0 0 0,则以该位 0 0 0 结尾的序列个数为 2 j − 1 2^{j-1} 2j−1 (左侧 j − 1 j-1 j−1 位 0 / 1 0/1 0/1 任意选择),求和即可。另外三种情况类似。前缀和预处理即可,对于每次询问查询,但因每次查询会实际修改,故使用树状数组(或线段树等) 维护单点修改前缀和。
我们计左侧为 l l l,右侧为 r r r,出现 0 / 1 0/1 0/1 以角标标注,如 l 0 l_0 l0 表示 左侧以 0 0 0 结尾的序列数。
| 序列类型 | 贡献影响 | 出现次数 |
|---|---|---|
| 0 ‾ \underline{0} 0 | 0 0 0 | 1 1 1 |
| ⋯ 0 0 ‾ \cdots0\underline{0} ⋯00 | + 1 +1 +1 | l 0 l_0 l0 |
| ⋯ 1 0 ‾ \cdots1\underline{0} ⋯10 | − 1 -1 −1 | l 1 l_1 l1 |
| 0 ‾ 0 ⋯ \underline{0}0\cdots 00⋯ | + 1 +1 +1 | r 0 r_0 r0 |
| 0 ‾ 1 ⋯ \underline{0}1\cdots 01⋯ | − 1 -1 −1 | r 1 r_1 r1 |
| ⋯ 0 0 ‾ 0 ⋯ \cdots0\underline{0}0\cdots ⋯000⋯ | + 2 +2 +2 | l 0 r 0 l_0r_0 l0r0 |
| ⋯ 0 0 ‾ 1 ⋯ \cdots0\underline{0}1\cdots ⋯001⋯ | 0 0 0 | l 0 r 1 l_0r_1 l0r1 |
| ⋯ 1 0 ‾ 0 ⋯ \cdots1\underline{0}0\cdots ⋯100⋯ | 0 0 0 | l 1 r 0 l_1r_0 l1r0 |
| ⋯ 1 0 ‾ 1 ⋯ \cdots1\underline{0}1\cdots ⋯101⋯ | − 2 -2 −2 | l 1 r 1 l_1r_1 l1r1 |
故将某位由 0 0 0 变 1 1 1 对答案产生的影响为 l 0 + r 0 + 2 l 0 r 0 − ( l 1 + r 1 + 2 l 1 r 1 ) l_0+r_0+2l_0r_0-(l_1+r_1+2l_1r_1) l0+r0+2l0r0−(l1+r1+2l1r1).
1 1 1 变 0 0 0 的情况对称即可。
- 时间复杂度: O ( n + q log n ) O(n+q\log{n}) O(n+qlogn)
- 空间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
#define fix(x) (((x) % MOD + MOD) % MOD)
using namespace std;
typedef long long LL;
const int MOD = 998244353;
const int N = 2e5 + 1;
LL n, res;
string s, str;
struct Tree {
LL bt[N];
void add(int x, LL num) {
for (; x <= n; x += (x & -x)) bt[x] = fix(bt[x] +num);
}
LL get(int x) {//[1,x]
LL res = 0;
for (; x; x -= (x & -x)) res = fix(res + bt[x]);
return res;
}
LL get(int l, int r) {//[l,r]
return fix(get(r) - get(l - 1));
}
} sum[4]; //l1,r1
LL pow_(LL b, LL p) {
LL res = 1 % MOD;
while (p) {
if (p & 1) (res *= b) %= MOD;
(b *= b) %= MOD;
p >>= 1;
}
return res;
}
void modify(int idx) {
LL l1 = sum[0].get(idx - 1);
LL r1 = sum[1].get(n - idx);
LL l0 = fix(pow_(2, idx - 1) - 1 - l1);
LL r0 = fix(pow_(2, n - idx) - 1 - r1);
LL ans = l0 + r0 + 2 * l0 * r0 - l1 - r1 - 2 * l1 * r1;
LL f = s[idx] == '0' ? 1 : -1;
res = fix(res + f * ans);
sum[0].add(idx, f * pow_(2, idx - 1));
sum[1].add(n - idx + 1, f * pow_(2, n - idx));
s[idx] ^= 1;
}
int main() {
cin.tie(NULL)->sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
int tt;
cin >> str >> tt;
n = str.length();
s = string(n + 1, '0');
res = pow_(2, n) - 1;
for (int i = 0; i <= n; i++)
sum[0].bt[i] = sum[1].bt[i] = 0;
for (int i = 0; i < n; i++)
if (str[i] == '1') modify(i + 1);
while (tt--) {
int idx;
cin >> idx;
modify(idx);
cout << res << ' ';
}
cout << '\n';
}
}
1166

被折叠的 条评论
为什么被折叠?



