A.Constructive Problems(思维)
题意:
给出一个 n × m n \times m n×m的网格,你需要将网格上所有点均填满水,当一个格子同时满足以下两个条件时,格子中也会被填满水:
-
该格子的左边或右边已经被填满水了
-
该格子的上面或下面已经被填满水了
一次操作可以给一个格子填满水,问至少几次操作才能将所有网格填满。
分析:
分析样例后可以发现,对于一个 n × m n \times m n×m的网格,每行/列均只需要包含一个被填满水的格子就能保证所有格子中均被水覆盖,即答案为 m a x ( n , m ) max(n, m) max(n,m)。
填充方案如下图:
代码:
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n, m;
cin >> n >> m;
cout << max(n, m) << endl;
}
int main() {
int Case;
cin >> Case;
while (Case--) {
solve();
}
return 0;
}
B.Begginer’s Zelda(思维)
题意:
给出一棵树,你可以继续若干次以下操作:
-
选择两个点 u u u和 v v v
-
然后将 u u u到 v v v之间的所有点删除(包括 u , v u,v u,v),然后添加一个新的点代替原本的所有点,并连接原本所有点连着的边。
问至少需要多少次操作,才能使树上只剩下一个节点?
分析:
观察样例后可以发现,每次选择两个不同链上的叶节点,删除他们到达根节点的边,此时,由于叶节点所在的链被删除了,那么删除后不会产生新的叶节点,即树上的叶节点数量会减少 2 2 2(当树上至少还包含 4 4 4个叶节点时,如果树上只包含 3 3 3个叶节点,不难发现删除后只会减少一个叶节点)。
那么,如果总叶结点数量为 k k k,将叶节点删除到只剩 2 2 2需要的次数为 ⌊ ( k − 1 ) / 2 ⌋ \lfloor(k - 1) / 2\rfloor ⌊(k−1)/2⌋,然后,只剩两个叶节点的树实际上就是一条链,此时再进行一次操作就能将树变为一个节点,因此,答案为 ⌊ ( k − 1 ) / 2 + 1 ⌋ = ⌊ ( k + 1 ) / 2 ⌋ \lfloor(k - 1) / 2 + 1\rfloor = \lfloor(k + 1) / 2\rfloor ⌊(k−1)/2+1⌋=⌊(k+1)/2⌋。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5e2;
int n, ans;
vector<int> G[N];
void dfs(int root, int pre) {
int len = G[root].size();
if (len == 1) ans++;
for (int i = 0; i < len; i++) {
int v = G[root][i];
if (v == pre) continue;
dfs(v, root);
}
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) G[i].clear();
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
ans = 0;
dfs(1, -1);
cout << (ans + 1) / 2 << endl;
}
int main() {
int Case;
cin >> Case;
while (Case--) {
solve();
}
return 0;
}
C.Largest Subsequence(贪心)
题意:
给出一个长度为 n n n的字符串 s s s,字符串中仅包含小写字母,你可以进行若干次以下操作:
- 选择字符串 s s s中的字典序最大的子序列 t t t,并将该子序列向右旋转一次,即将 t 1 t 2 . . . t m t_1t_2...t_m t1t2...tm变为 t m t 1 t 2 . . . t m − 1 t_mt_1t_2...t_{m - 1} tmt1t2...tm−1
问:最少操作几次可以使得该字符串被排序成增序,如果无法完成排序,输出 − 1 -1 −1.
分析:
不难发现,由于每次右移相当于在子序列中将最后的元素删除,那么操作后子序列仍然是字典序最大的,实际上能进行操作的始终是同一个字符串。
那么,只需要对该子序列进行排序,然后检查字符串排序后整个字符串 s s s是否有序,如果有序,输出操作前字典序最大的子序列中包含的所有字符数量减去最大字符的数量(排序执行到最大元素到最后位置就结束了,因此该子序列中所有最大的元素是不需要操作次数的)。
否则,输出 − 1 -1 −1
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5e2;
vector<int> V[30];
void solve() {
int n;
string s;
cin >> n >> s;
for (int i = 0; i < 26; i++) V[i].clear();
for (int i = 0; i < n; i++) {
V[s[i] - 'a'].push_back(i);
}
int start_pos = -1, cnt = 0;
vector<int> p;
for (int i = 25; i >= 0; i--) {
int pos = lower_bound(V[i].begin(), V[i].end(), start_pos) - V[i].begin();
int len = V[i].size();
if (pos >= len) continue;
if (cnt == 0) cnt = V[i].size();
for (int j = pos; j < len; j++) {
p.push_back(V[i][j]);
}
start_pos = *(--p.end());
}
int len = p.size();
for (int i = 0, j = len - 1; i < j; i++, j--) {
swap(s[p[i]], s[p[j]]);
}
for (int i = 1; i < n; i++) {
if (s[i] < s[i - 1]) {
cout << "-1" << endl;
return;
}
}
cout << p.size() - cnt << endl;
}
int main() {
int Case;
cin >> Case;
while (Case--) {
solve();
}
return 0;
}
D.Cyclic MEX(单调栈)
题意:
对于数组 a a a,定义其代价为 ∑ i = 1 n m e x † ( [ a 1 , a 2 , … , a i ] ) \sum_{i=1}^{n}mex†([a_1,a_2,…,a_i]) ∑i=1nmex†([a1,a2,…,ai])。
给定集合 { 0 , 1 , 2 , … , n − 1 } \left\{0,1,2,…,n−1\right\} {0,1,2,…,n−1}的一个排列 ‡ p ‡p ‡p,求 p p p所有循环移位的最大代价。
† m e x ( [ b 1 , b 2 , … , b m ] ) †mex([b_1,b_2,…,b_m]) †mex([b1,b2,…,bm])是不存在于 b 1 , b 2 , … , b m b_1,b_2,…,b_m b1,b2,…,bm中的最小非负整数 x x x。
排列
‡
A
‡A
‡A
{
0
,
1
,
2
,
…
,
n
−
1
}
\left\{0,1,2,…,n−1\right\}
{0,1,2,…,n−1}是由
n
n
n个元素组成的数组
,包含从
0
0
0到
n
−
1
n−1
n−1的任意顺序的不同整数。例如,
[
1
,
2
,
0
,
4
,
3
]
[1,2,0,4,3]
[1,2,0,4,3]是一个排列,但
[
0
,
1
,
1
]
[0,1,1]
[0,1,1]不是一个排列(
1
1
1在数组中出现两次),
[
0
,
2
,
3
]
[0,2,3]
[0,2,3]也不是一个排列(
n
=
3
n=3
n=3,但数组中有
3
3
3)。
分析:
对于本题目,不妨先想一个最简单的情况: 0 0 0在数组的最后一个位置,那么显然除了最后一个位置的前缀 m e x mex mex是 n n n,其他所有位置的前缀 m e x mex mex都是 0 0 0。
一个一个地把当前数组的第一个数字移动到最后面,观察一下这会对所有 n n n个位置的前缀 m e x mex mex造成什么影响:
假设某一次移动的数字是 x x x,设 f i f_i fi为移动前数组前缀 [ 1 , i ] [1,i] [1,i]的 m e x mex mex值,对于所有满足 f i > x f_i>x fi>x的 i i i,我们将 x x x移动到后面之后, x x x便不会存在于这些前缀里面了,因此它们的 f i f_i fi会变为 x x x。
对于满足 f i < x f_i < x fi<x 的 i i i,将 x x x移动到后面之后不会对它们造成影响。
模拟一下上述的过程就会发现这是一个类似单调栈的形式,维护一个单调栈,放入(值,值对应的一段区间的最右位置)。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
LL n,id,a[3000005];
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL);cout.tie(NULL);
int t;
cin>>t;
while(t--){
cin>>n;
id=0;
for(LL i=1;i<=n;++i){
cin>>a[i];
a[i+n]=a[i+2*n]=a[i];
}
for(LL i=n+1;i<=2*n;++i)
if(a[i]==0)
id=i;
LL sum,ans;
sum=ans=n;
map<LL,LL>cnt;
++cnt[n];
for(LL i=id-n+1,j=1;j<=n-1;++i,++j){
while((*cnt.rbegin()).first>a[i]){
LL x=(*cnt.rbegin()).first,c=(*cnt.rbegin()).second;
cnt.erase(x),sum-=c*x;
cnt[a[i]]+=c,sum+=c*a[i];
}
sum+=n,++cnt[n],ans=max(ans,sum);
}
cout<<ans<<endl;
}
return 0;
}
E.One-X(树形DP)
题意:
线段树是一种树,其中每个节点代表一个区间并具有其编号。通过递归可以使用一个包含n个元素的数组构建一个线段树。假设函数 b u i l d ( v , l , r ) build(v,l,r) build(v,l,r),作用是构建以编号 v v v为根节点,包含区间 [ l , r ] [l,r] [l,r]的线段树。
现在让我们定义 b u i l d ( v , l , r ) build(v,l,r) build(v,l,r):
- 如果 l = r l=r l=r,则该节点 v v v是叶子节点,停止添加更多边。
- 否则,添加边 ( v , 2 v ) (v,2v) (v,2v)和 ( v , 2 v + 1 ) (v,2v+1) (v,2v+1)。让 m = ⌊ l + r 2 ⌋ m=⌊l+r2⌋ m=⌊l+r2⌋。然后调用 b u i l d ( 2 v , l , m ) build(2v,l,m) build(2v,l,m)和 b u i l d ( 2 v + 1 , m + 1 , r ) build(2v+1,m+1,r) build(2v+1,m+1,r)。
因此,整棵树通过调用 b u i l d ( 1 , 1 , n ) build(1,1,n) build(1,1,n)来构建。
现在将为具有 n n n个元素的数组构造一个线段树,计算 l c a † ( S ) lca†(S) lca†(S)的总和,其中 S S S是非空叶子节点的集合。题目保证恰好有 2 n − 1 2n−1 2n−1个可能的子集合。由于这个总和可能非常大,输出它模 998244353 998244353 998244353的结果。
l c a † ( S ) lca†(S) lca†(S)是 S S S中节点的最小公共祖先数目。
分析:
尝试递推来缩小规模,考虑一个结点,编号为 X X X,其作为 L C A LCA LCA产生的总和为 X ⋅ ( 2 左子树区间长度 − 1 ) ⋅ ( 2 右子树区间长度 − 1 ) X⋅(2^{左子树区间长度}−1)⋅(2^{右子树区间长度}−1) X⋅(2左子树区间长度−1)⋅(2右子树区间长度−1)。那么对于一个子树 S S S其内部所有节点产生的总和,只跟这个子树的区间长度和子树的根节点有关。那么对于一棵区间长度为 L L L的子树,可以用 k k k和 b b b表示,当这个子树的根节点值为 x x x时整个子树的 L C A LCA LCA总和为 k x + b kx+b kx+b。
对一个区间长度为 L L L的子树,可以通过其两个子树的 k k k和 b b b得到其自身的 k k k和 b b b,计算过程如下:
{ k = 2 k l + 2 k r + ( 2 左子树区间长度 − 1 ) ⋅ ( 2 右子树区间长度 − 1 ) b = b l + b r + k l \begin{cases} k=2k_l+2k_r+(2^\text{左子树区间长度}−1)⋅(2^\text{右子树区间长度}−1) \\ b=b_l+b_r+k_l \end{cases} {k=2kl+2kr+(2左子树区间长度−1)⋅(2右子树区间长度−1)b=bl+br+kl
线段树有一个性质是,若总区间长度为 n n n,对于第 x x x层(设根节点为第 0 0 0层),节点的区间长度一定是 ⌊ n 2 x ⌋ ⌊\frac{n}{2^x}⌋ ⌊2xn⌋和 ⌈ n 2 x ⌉ ⌈\frac{n}{2^x}⌉ ⌈2xn⌉中的一个,因此一个这样的线段树最多可产生 2 log 2 n 2\log_2n 2log2n个不同的 ( k , b ) (k,b) (k,b)数对。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
typedef long long LL;
struct s1 {
LL k, b;
s1(LL k1 = 0, LL b1 = 0) : k(k1), b(b1) {}
LL operator()(LL x) {
return (k * x + b) % MOD;
}
s1 operator+(s1 tmp) {
return s1((k + tmp.k) % MOD, (b + tmp.b) % MOD);
}
};
map<LL, s1> m1;
LL pow_mod(LL a, LL b) {
b %= MOD - 1;
LL ret = 1;
while (b) {
if (b & 1) ret = (ret * a) % MOD;
a = (a * a) % MOD;
b >>= 1;
}
return ret;
}
s1 solve(LL n) {
if (n == 1)
return s1(1, 0);
if (m1.count(n))
return m1[n];
LL mid = n >> 1;
s1 resl = solve(n - mid);
s1 resr = solve(mid);
LL k = resl.k * 2 + resr.k * 2 + (pow_mod(2, n - mid) - 1) * (pow_mod(2, mid) - 1);
LL b = resl.b + resr.b + resr.k;
return m1[n] = s1((k % MOD + MOD) % MOD, b % MOD);
}
int main() {
int T;
LL n;
cin >> T;
while (T--) {
cin >> n;
cout << solve(n)(1) << endl;
}
return 0;
}
F.Field Should Not Be Empty(MAP+遍历)
题意:
给你一个长度为 n n n的排列 † p †p †p。
如果对于所有 y < x y < x y<x,都满足 p y < p x p_y < p_x py<px,并且对于所有 y > x y>x y>x都满足 p y > p x p_y > p_x py>px,认为称点 x x x是好数。本题定义 f ( p ) f(p) f(p)为 p p p中好数的个数。
可以执行以下操作:选择两个不同的值 i i i和 j j j,并交换元素 p i p_i pi和 p j p_j pj。
应用上述操作一次,求 f ( p ) f(p) f(p)的最大值。
长度为 n n n的排列 † A †A †A是指由 n n n个不同的整数 1 − n 1-n 1−n组成的数组,每个数字位置随机。例如, [ 2 , 3 , 1 , 5 , 4 ] [2,3,1,5,4] [2,3,1,5,4]是一个排列,但 [ 1 , 2 , 2 ] [1,2,2] [1,2,2]不是一个排列( 2 2 2在数组中出现两次), [ 1 , 3 , 4 ] [1,3,4] [1,3,4]也不是一个排列( n = 3 n=3 n=3,但数组中有 4 4 4)。
分析:
一个点是好数的条件就是 a i = i a_i=i ai=i,且小于其的数都在其左侧,大于其的数都在其右侧。
首先如果已经是有序排列了,那么答案是 n − 2 n-2 n−2,需要特判这种情况。
对于本题,执行把一个小的数交换到大的数后面去的操作,这不会使 f ( p ) f(p) f(p)变得更大,所以不会执行。同理,把一个大的数放到后面去不会使 f ( p ) f(p) f(p)变得更小,所以最初是好数的点操作完仍然还是好数。
本题只能交换一次,其实可以发现选择是有限的。我们会进行的操作,其结果只有两种可能
- 操作 a i , a a_i,a ai,a,会让 a i , a a_i,a ai,a中间的某个位置变成好数
- 操作 a i , a a_i,a ai,a,会让 a i , a a_i,a ai,a其中某个点本身变成好数
对于某个点,如果已经满足 a i = i a_i=i ai=i,我们看一下它的前面有多少个数大于它,如果没有,那么已经满足要求,如果有一个,可以找到需要交换的位置,记录下来,否则不可能在一次操作内让这个点变成好数,忽略即可。
所以,可能对 f ( p ) f(p) f(p)产生贡献的位置对,只有 2 ∗ n 2*n 2∗n个,暴力检查这 2 ∗ n 2*n 2∗n个,算出贡献改变量即可。
统计一下序列中已经是好数的位置,计这个个数是 c n t cnt cnt
-
如果所有位置都归位,那么交换会损失两个,也就是 c n t − 2 cnt-2 cnt−2
-
至少有一个没归位的。如果交换一个归位的和一个没归位的,相邻就会只损失一个位置,答案至少为 c n t − 1 cnt-1 cnt−1。在本题中,没归位的是成对出现的,并且一定存在没归位的两个位置是逆序的(假设都是正序的,那么就都归位了,矛盾)。交换这两个逆序位置, c n t cnt cnt不变,所以答案可以取到 c n t cnt cnt
-
当交换 ( x , y ) ( x < y ) (x,y)(x < y) (x,y)(x<y)两个位置时, [ 1 , x − 1 ] [1,x-1] [1,x−1]的位置不受影响, [ y + 1 , n ] [y+1,n] [y+1,n]的位置不会受影响。由前文分析得 p x > p y p_x>p_y px>py一定成立。考虑 [ x + 1 , y − 1 ] [x+1,y-1] [x+1,y−1]这个区间里的位置 i i i,在 ( x , y ) (x,y) (x,y)交换前,是没有好数的,因为若将 ( i , p i ) (i,p_i) (i,pi)看成是二维平面点的时候,左侧的点都在左下方,右侧的点都在右上方,也就意味着,当 i i i左侧的值和 i i i右侧的值没有形成逆序对时,才会出现好数,而 p x p_x px和 p y p_y py是一对逆序对,所以没有好数位置。
枚举所有可能增加的位置对,将对应贡献加上,共有 2 ∗ n 2*n 2∗n种情况,遍历取最大值即可。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main() {
int t;
cin>>t;
while(t--){
int n;
cin>>n;
vector<int> p(n),invp(n);
for(int i=0;i<n;i++) {
cin>>p[i];
p[i]--;
invp[p[i]] = i;
}
vector<array<int, 2>>pre(n, {-1, -1}),suf(n, {n, n});
for (int i=1;i<n;i++) {
pre[i]=pre[i-1];
int x=p[i-1];
for(int j=0;j<2;j++) {
if(x>pre[i][j]) {
swap(x,pre[i][j]);
}
}
}
for (int i=n-2;i>=0;i--) {
suf[i]=suf[i+1];
int x=p[i+1];
for (int j=0;j<2;j++) {
if(x<suf[i][j]) {
swap(x, suf[i][j]);
}
}
}
vector<int>bad(n);
int cnt=0;
int ans=0;
map<pair<int, int>, int> mp;
for(int i=0;i<n;i++) {
if (p[i]==i) {
if(pre[i][0] < i && suf[i][0] > i){
bad[i] = 1;
cnt += 1;
}
else if (pre[i][1] < i && suf[i][1] > i) {
mp[{invp[pre[i][0]], invp[suf[i][0]]}] += 1;
}
}
else if (invp[i] < i) {
if (suf[i][0] > i && pre[i][0] == i) {//i值向后换
mp[{invp[i], i}] += 1;
}
}
else {
if (suf[i][0] == i && pre[i][0] < i) {//i值向前换
mp[{i, invp[i]}] += 1;
}
}
}
ans = cnt - 2;
if (cnt < n) {
ans = cnt;
}
for(auto [s, c] : mp) {//换(s_x,s_y]能增加c个归位的,换一对(x,y),x以左y以右不会受到影响,但是会减少[bad[x],bad[y]]个归位的
auto[x, y]=s;
ans=max(ans, cnt + c);
}
cout<<ans<<"\n";
}
return 0;
}
学习交流
以下为学习交流QQ群,群号: 546235402,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。