A.Please Sign(图论)
题意:
给定长度为 n n n的数组 a a a,长度为 n − 1 n-1 n−1的数组 p p p,现在执行无数次以下操作:
- 对于 2 ≤ i ≤ n 2 \le i \le n 2≤i≤n ,按顺序执行 a p i = a p i + a i a_{p_i}=a_{p_i}+a_i api=api+ai。
确定最后 a 1 a_1 a1是正数还是负数还是 0 0 0。
分析:
考虑所有 i i i向 p i p_i pi建边,发现是一棵以 1 1 1为根的树,再计算每个节点距离 1 1 1的深度,同一深度的权重相同,因此需将每个深度的节点的权值和都相加,如果权值和为 0 0 0 ,说明这一深度层无用,否则最终答案的正负取决于最深深度节点的权值和。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2.5e5 + 5;
const int inf = 0x3f3f3f3f;
LL n, a[N], p[N], f[N], dep[N], mx;
vector<int> e[N];
void dfs(int u) {
mx = max(mx, dep[u]);
for (auto v: e[u]) {
dep[v] = dep[u] + 1;
dfs(v);
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 2; i <= n; i++)
cin >> p[i], e[p[i]].push_back(i);
dfs(1);
for (int i = 2; i <= n; i++)
f[dep[i]] += a[i];
int flag = 0;
for (int i = n; i; i--)
if (f[i]) {
flag = f[i] > 0 ? 1 : -1;
break;
}
a[1] = flag * inf + a[1];
if (a[1] > 0)
cout << "+";
else if (a[1] == 0)
cout << "0";
else
cout << "-";
return 0;
}
B.Subsegments with Small Sums(思维)
题意:
给出正整数 s s s,和数列 a a a,记 f ( a ) f(a) f(a)表示:将序列分成 x x x块子序列,并保证每一块子序列的和都小于等于 s s s。记 f ( a ) f(a) f(a)为其中最小的 x x x。
现在给定长度为 n n n的数列 a a a,求 ∑ 1 ≤ l ≤ r ≤ n f ( ( a l , a l + 1 , ⋯ , a r ) ) ∑1≤l≤r≤nf((a_l,a_{l+1},⋯,a_r)) ∑1≤l≤r≤nf((al,al+1,⋯,ar)).
分析:
考虑贪心,从左往右考虑这个序列,如果上一段能放下就放进上一段,否则新开一段。再考虑左端点相同时每个右端点的答案,设这个和为 f l f_l fl。分为两种情况:
- 只有一段,对答案的贡献为 1 1 1。
- 有至少两段,但根据上面的贪心,它们第一段结束的位置均相同。设这个位置是 x x x,那么满足 r ≥ x r \ge x r≥x的区间可以看作 [ x , r ] [x,r] [x,r]答案 + 1 +1 +1,即 f l = f x + ( n − l + 1 ) f_l=f_x+(n-l+1) fl=fx+(n−l+1)。
求和的 a n s = ∑ i = 1 i = n f i ans=\sum\limits_{i=1}^{i=n}f_i ans=i=1∑i=nfi。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
const int inf = 0x3f3f3f3f;
LL a[N], sum[N], f[N];
int main() {
LL n, s;
cin >> n >> s;
for (int i = 1; i <= n; i++)
cin >> a[i], sum[i] = sum[i - 1] + a[i];
for (int i = n; i; i--) {
int nxt = lower_bound(sum + 1, sum + n + 1, sum[i - 1] + s + 1) - sum;
f[i] = f[nxt] + (n - i + 1);
}
LL ans = 0;
for (int i = 1; i <= n; i++)
ans += f[i];
cout << ans << endl;
return 0;
}
C.Not So Consecutive(dp)
题意:
给出长度为 n n n的序列 a a a,定义一个序列 a a a是好的,要求:
- 1 ≤ a i ≤ n ≤ 5000 1 \le a_i \le n \le 5000 1≤ai≤n≤5000
- 序列 a a a中没有任何一个数字 x x x的连续出现次数 > x >x >x。
现在给定一个长度为 n n n的序列 a a a,序列中的元素不是 − 1 -1 −1就是 1 − n 1-n 1−n中的任意一个数,请将所有的 − 1 -1 −1替换成 1 − n 1-n 1−n的任意数字,计算最后结果有多少不同的序列 a a a是好的,并将结果对 998244353 998244353 998244353进行取模。
分析:
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示填了前
i
i
i个位置,第
j
j
j个位置填的数字是
j
j
j的方案数。枚举这个连续段的起点,得到转移为:
f
i
,
j
=
∑
k
=
m
a
x
(
0
,
i
−
j
)
i
−
1
[
∀
l
∈
[
k
+
1
,
i
]
,
A
l
=
−
1
o
r
A
l
=
j
]
∑
c
o
l
≠
j
f
k
,
c
o
l
f_{i,j}= \sum\limits_{k=max(0,i-j)}^{i-1}[∀l∈[k+1,i],A_l=−1 or A_l=j]\sum\limits_{col\neq j}f_{k,col}
fi,j=k=max(0,i−j)∑i−1[∀l∈[k+1,i],Al=−1orAl=j]col=j∑fk,col。
方括号里的式子是为了找
i
i
i前面最后一个填了不为
j
j
j 的数的位置,设这个位置为
l
s
t
lst
lst。对每个数维护
p
o
s
j
pos_j
posj 表示数
j
j
j当前最后一次出现的位置。那么对于每个新的
i
i
i,我们记录
p
o
s
pos
pos 的最大值和次大值。
同时将
c
o
l
≠
j
col \neq j
col=j这个条件容斥掉,得到最后转移方程为:
f
i
,
j
=
∑
k
=
l
s
t
i
−
1
∑
c
o
l
=
1
n
f
k
,
c
o
l
−
∑
k
=
l
s
t
i
−
1
f
k
,
j
f_{i,j}=\sum\limits_{k=lst}^{i-1} \sum\limits_{col=1}^{n} f_{k,col}-\sum\limits_{k=lst}^{i-1}f_{k,j}
fi,j=k=lst∑i−1col=1∑nfk,col−k=lst∑i−1fk,j
记
s
i
,
j
=
∑
k
=
1
i
f
k
,
j
s_{i,j}=\sum\limits_{k=1}^{i}f_{k,j}
si,j=k=1∑ifk,j,
s
u
m
i
=
∑
j
=
1
i
∑
c
o
l
=
1
n
f
i
,
c
o
l
sum_i=\sum\limits_{j=1}^{i} \sum\limits_{col=1}^{n}f_{i,col}
sumi=j=1∑icol=1∑nfi,col
前缀和优化,
n
2
n^2
n2转移即可。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5e3 + 5;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
int pos[N], f[N][N], s[N][N], sum[N], n, a[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
f[0][0] = 1, sum[0] = 1;
for (int i = 1; i <= n; i++) {
if (a[i] != -1)
pos[a[i]] = i;
int mx1 = 0, mx2 = 0;
for (int j = 1; j <= n; j++) {
if (pos[j] > mx1)
mx2 = mx1, mx1 = pos[j];
else if (pos[j] > mx2)
mx2 = pos[j];
}
for (int j = 1; j <= n; j++) {
int lst = max(i - j, (a[mx1] == j) ? mx2 : mx1);
f[i][j] = sum[i - 1] - (lst ? sum[lst - 1] : 0) - (s[i - 1][j] - (lst ? s[lst - 1][j] : 0));
f[i][j] = (f[i][j] % mod + mod) % mod;
s[i][j] = (s[i - 1][j] + f[i][j]) % mod;
(sum[i] += f[i][j]) %= mod;
}
(sum[i] += sum[i - 1]) %= mod;
}
cout << (sum[n] - sum[n - 1] + mod) % mod << endl;
return 0;
}
D Add to Make a Permutation(思维)
题意:
给你一个序列 a a a, 0 ≤ a i ≤ n − 1 0 \le a_i \le n-1 0≤ai≤n−1,你可以进行任意次以下操作:
- 任意选择 m m m个元素,把他们的值增加 1 1 1并对 n n n取模。
询问能否通过任意次操作使得 a a a变成一个 0 − n − 1 0-n-1 0−n−1的排列。
分析:
设序列 a a a 操作得到未被取模的最终序列 b b b
b b b一定满足以下条件:
-
b i ≤ a i b_i \le a_i bi≤ai
-
b i % n b_i \% n bi%n两两不同
-
令 s = ∑ i = 1 n ( b i − a i ) s=\sum\limits_{i=1}^{n}(b_i-a_i) s=i=1∑n(bi−ai),有 s % m = 0 s\%m=0 s%m=0
-
m a x ( b i − a i ) ≤ s m max({b_i−a_i}) \le \frac{s}{m} max(bi−ai)≤ms
在一定的操作次数下 m a x ( b ∗ i − a i ) max({b*i−a_i}) max(b∗i−ai) 越小越容易满足条件。将 a a a序列升序排序,最优的对应关系一定是将 b b b也升序排序。
发现答案只和 s s s 的值有关,有结论:若有解,一定存在一种最优方案形如 b = ( x , x + 1 , ⋯ , x + n ) b=(x,x+1,⋯,x+n) b=(x,x+1,⋯,x+n)
证明:假设我们有一个最优的 b b b 序列,且 b n − b 1 b_n-b_1 bn−b1>n。那么令 b 1 ← b n − n b_1←b_n−n b1←bn−n, b n ← b 1 + n b_n←b_1+n bn←b1+n,依次对照上面的所有条件:
-
因为 b n − n > b 1 b_n−n>b_1 bn−n>b1,新的 b 1 b_1 b1 比原来大。同时因为 b 1 > a 1 b_1>a_1 b1>a1 , a n − a 1 < n a_n − a_1 < n an−a1<n,有 b 1 + n > a n b_1+n>a_n b1+n>an,满足条件 1 1 1
-
加减 n n n不改变取模后的值。
-
总和没变,S 不变。
-
b n − a n b_n-a_n bn−an 一定变小。前者有 b n − a n > b n − n − a 1 b_n-a_n>b_n-n-a_1 bn−an>bn−n−a1,所以也变小,满足条件。
那么我们只需考虑最小化
x
x
x的值。根据第一个条件可以得到
x
x
x的下界。第四个条件
x
x
x每增加 1,不等式左侧增加
1
1
1,右侧增加
n
m
\frac{n}{m}
mn。因为
n
>
m
n>m
n>m,第四个条件也为
x
x
x提供了下界。
接下来令
x
x
x满足第三个条件即可,发现
x
x
x 和
x
+
n
x+n
x+n 是等价的,
x
x
x 可以直接枚举
n
n
n次。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
const int mod = 998244353;
LL n, m, a[N], sum, x;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++)
x = max(x, a[i] - i + 1);
for (int i = 1; i <= n; i++)
sum += (x + i - 1) - a[i];
bool flag = 0;
for (int i = 0; i <= n; i++)
if ((sum + n * i) % m == 0) {
x += i, sum += n * i, flag = 1;
break;
}
if (!flag) {
cout << -1 << endl;
return 0;
}
LL mx = 0;
for (int i = 1; i <= n; i++)
mx = max(mx, x + i - 1 - a[i]);
while (mx > sum / m)
mx += m / __gcd(n, m), sum += n * m / __gcd(n, m);
cout << sum / m << endl;
return 0;
}
E Avoid Boring Matches(思维)
题意:
有 2 n 2^n 2n个人,参加一项锦标赛。规则如下:
- 每个人都有一顶颜色为红色或者蓝色的帽子,由字符串 S S S给出。
- 重复以下操作,直到只剩下一个参与者:假设 2 k 2k 2k是当前的参与者人数。将参与者分成两组。可以自由选择如何配对它们。 然后,每组举行一场比赛,赢家留下来,输家离开比赛。 参与者按强度降序编号,因此编号较小的参与者总是获胜。
两个戴着红帽子的参与者之间的比赛称为无聊的比赛。 你的目标是安排配对,这样在比赛期间就不会发生无聊的比赛。
现在你可以执行任意次以下操作:
- 选择 S S S中相邻的两个字符并交换他们。
询问是否有可能实现目标。 如果是,请查找所需的最小操作数。
分析:
R
R
R 比
B
B
B 多一定无解,否则可以换成
B
B
B
B
.
.
.
R
R
R
BBBB...RRR
BBBB...RRR的形式,一定有解。
先找判断序列合法的充要条件。考虑每轮的最优匹配策略,我们希望更多的
B
B
B能留到下一轮,因此要尽可能地让
B
B
B 和它后面的
R
R
R 配对。
在多种配对方案能保留的 $B $数量相同时,我们希望留下的 $B $位置尽可能靠前,因为这样在下下轮它们被保留下来的概率更大。因此我们得到这样的贪心策略:从左到右考虑每个 B B B,将它和右边第一个未被配对的 R R R配对。最终将所有剩下未配对的数两两配对。根据上文可以知道这样做是最优的。设 t i t_i ti表示长度为 2 i 2^i 2i,且为所有 B B B尽可能靠右的合法解。
令 t 0 t_0 t0= R R R,考虑从 t i − 1 t_{i−1} ti−1 得到 t i t_i ti。从左到右处理 t i − 1 t_{i−1} ti−1 的每一位,若当前位是 R R R,表示这位没被匹配, t i = t i + R t_i=t_i+R ti=ti+R;当前位是 B B B,我们希望下一个 B B B尽量靠后,即与之匹配的 R R R尽量更近, t i = t i + B R t_i=t_i+BR ti=ti+BR。不足 2 i 2^i 2i 位用 B B B 补齐。
设 t n t_n tn的第 j j j 个 B B B 的位置为 T j T_j Tj,原序列位置为 S j S_j Sj。那么若存在 S j > T j S_j>T_j Sj>Tj,则该序列不合法。证明大概是如果一个 S j < T j S_j < T_j Sj<Tj,匹配数不会变多;但如果 S j > T j , B R S_j > T_j,BR Sj>Tj,BR的匹配数一定减少。
答案是令 S S S 满足上述条件的最小操作次数 a n s = ∑ j = 1 2 n m a x ( 0 , T j − S j ) ans=\sum\limits_{j=1}^{2^n}max(0,T_j-S_j) ans=j=1∑2nmax(0,Tj−Sj)
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int inf = 0x3f3f3f3f;
const int N = (1 << 18) + 5;
int n;
string s, t[N];
int tot, pos[N];
int main() {
cin >> n;
cin >> s;
t[0] = "R";
for (int i = 1; i <= n; i++) {
for (auto c: t[i - 1])
if (c == 'R')
t[i] += "R";
else
t[i] += "BR";
while (t[i].size() < (1 << i))
t[i] += "B";
}
for (int i = 0; i < (1 << n); i++)
if (t[n][i] == 'B')
pos[++tot] = i;
tot = 0;
long long ans = 0;
for (int i = 0; i < (1 << n); i++)
if (s[i] == 'B') {
tot++;
if (i > pos[tot])
ans += i - pos[tot];
if (tot == (1 << n - 1))
break;
}
if (tot < (1 << n - 1))
cout << -1 << endl;
else
cout << ans << endl;
return 0;
}
学习交流
以下为学习交流QQ群,群号: 546235402,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。