A.Constructive Problems(思维)
题意:
给出一个n×mn \times mn×m的网格,你需要将网格上所有点均填满水,当一个格子同时满足以下两个条件时,格子中也会被填满水:
-
该格子的左边或右边已经被填满水了
-
该格子的上面或下面已经被填满水了
一次操作可以给一个格子填满水,问至少几次操作才能将所有网格填满。
分析:
分析样例后可以发现,对于一个n×mn \times mn×m的网格,每行/列均只需要包含一个被填满水的格子就能保证所有格子中均被水覆盖,即答案为max(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(思维)
题意:
给出一棵树,你可以继续若干次以下操作:
-
选择两个点uuu和vvv
-
然后将uuu到vvv之间的所有点删除(包括u,vu,vu,v),然后添加一个新的点代替原本的所有点,并连接原本所有点连着的边。
问至少需要多少次操作,才能使树上只剩下一个节点?
分析:
观察样例后可以发现,每次选择两个不同链上的叶节点,删除他们到达根节点的边,此时,由于叶节点所在的链被删除了,那么删除后不会产生新的叶节点,即树上的叶节点数量会减少222(当树上至少还包含444个叶节点时,如果树上只包含333个叶节点,不难发现删除后只会减少一个叶节点)。
那么,如果总叶结点数量为kkk,将叶节点删除到只剩222需要的次数为⌊(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(贪心)
题意:
给出一个长度为nnn的字符串sss,字符串中仅包含小写字母,你可以进行若干次以下操作:
- 选择字符串sss中的字典序最大的子序列ttt,并将该子序列向右旋转一次,即将t1t2...tmt_1t_2...t_mt1t2...tm变为tmt1t2...tm−1t_mt_1t_2...t_{m - 1}tmt1t2...tm−1
问:最少操作几次可以使得该字符串被排序成增序,如果无法完成排序,输出−1-1−1.
分析:
不难发现,由于每次右移相当于在子序列中将最后的元素删除,那么操作后子序列仍然是字典序最大的,实际上能进行操作的始终是同一个字符串。
那么,只需要对该子序列进行排序,然后检查字符串排序后整个字符串sss是否有序,如果有序,输出操作前字典序最大的子序列中包含的所有字符数量减去最大字符的数量(排序执行到最大元素到最后位置就结束了,因此该子序列中所有最大的元素是不需要操作次数的)。
否则,输出−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(单调栈)
题意:
对于数组aaa,定义其代价为∑i=1nmex†([a1,a2,…,ai])\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,求ppp所有循环移位的最大代价。
†mex([b1,b2,…,bm])†mex([b_1,b_2,…,b_m])†mex([b1,b2,…,bm])是不存在于b1,b2,…,bmb_1,b_2,…,b_mb1,b2,…,bm中的最小非负整数xxx。
排列‡A‡A‡A{0,1,2,…,n−1}\left\{0,1,2,…,n−1\right\}{0,1,2,…,n−1}是由nnn个元素组成的数组
,包含从000到n−1n−1n−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]不是一个排列(111在数组中出现两次),[0,2,3][0,2,3][0,2,3]也不是一个排列(n=3n=3n=3,但数组中有333)。
分析:
对于本题目,不妨先想一个最简单的情况:000在数组的最后一个位置,那么显然除了最后一个位置的前缀mexmexmex是nnn,其他所有位置的前缀mexmexmex都是000。
一个一个地把当前数组的第一个数字移动到最后面,观察一下这会对所有nnn个位置的前缀mexmexmex造成什么影响:
假设某一次移动的数字是xxx,设fif_ifi为移动前数组前缀[1,i][1,i][1,i]的mexmexmex值,对于所有满足fi>xf_i>xfi>x的iii,我们将xxx移动到后面之后,xxx便不会存在于这些前缀里面了,因此它们的fif_ifi会变为xxx。
对于满足 fi<xf_i < xfi<x 的iii,将xxx移动到后面之后不会对它们造成影响。
模拟一下上述的过程就会发现这是一个类似单调栈的形式,维护一个单调栈,放入(值,值对应的一段区间的最右位置)。
代码:
#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个元素的数组构建一个线段树。假设函数build(v,l,r)build(v,l,r)build(v,l,r),作用是构建以编号vvv为根节点,包含区间[l,r][l,r][l,r]的线段树。
现在让我们定义build(v,l,r)build(v,l,r)build(v,l,r):
- 如果l=rl=rl=r,则该节点vvv是叶子节点,停止添加更多边。
- 否则,添加边(v,2v)(v,2v)(v,2v)和(v,2v+1)(v,2v+1)(v,2v+1)。让m=⌊l+r2⌋m=⌊l+r2⌋m=⌊l+r2⌋。然后调用build(2v,l,m)build(2v,l,m)build(2v,l,m)和build(2v+1,m+1,r)build(2v+1,m+1,r)build(2v+1,m+1,r)。
因此,整棵树通过调用build(1,1,n)build(1,1,n)build(1,1,n)来构建。
现在将为具有nnn个元素的数组构造一个线段树,计算lca†(S)lca†(S)lca†(S)的总和,其中SSS是非空叶子节点的集合。题目保证恰好有2n−12n−12n−1个可能的子集合。由于这个总和可能非常大,输出它模998244353998244353998244353的结果。
lca†(S)lca†(S)lca†(S)是SSS中节点的最小公共祖先数目。
分析:
尝试递推来缩小规模,考虑一个结点,编号为XXX,其作为LCALCALCA产生的总和为 X⋅(2左子树区间长度−1)⋅(2右子树区间长度−1)X⋅(2^{左子树区间长度}−1)⋅(2^{右子树区间长度}−1)X⋅(2左子树区间长度−1)⋅(2右子树区间长度−1)。那么对于一个子树SSS其内部所有节点产生的总和,只跟这个子树的区间长度和子树的根节点有关。那么对于一棵区间长度为LLL的子树,可以用kkk和bbb表示,当这个子树的根节点值为xxx时整个子树的LCALCALCA总和为kx+bkx+bkx+b。
对一个区间长度为LLL的子树,可以通过其两个子树的kkk和bbb得到其自身的kkk和bbb,计算过程如下:
{k=2kl+2kr+(2左子树区间长度−1)⋅(2右子树区间长度−1)b=bl+br+kl\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
线段树有一个性质是,若总区间长度为nnn,对于第xxx层(设根节点为第000层),节点的区间长度一定是⌊n2x⌋⌊\frac{n}{2^x}⌋⌊2xn⌋和⌈n2x⌉⌈\frac{n}{2^x}⌉⌈2xn⌉中的一个,因此一个这样的线段树最多可产生2log2n2\log_2n2log2n个不同的(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+遍历)
题意:
给你一个长度为nnn的排列†p†p†p。
如果对于所有y<xy < xy<x,都满足py<pxp_y < p_xpy<px,并且对于所有y>xy>xy>x都满足py>pxp_y > p_xpy>px,认为称点xxx是好数。本题定义f(p)f(p)f(p)为ppp中好数的个数。
可以执行以下操作:选择两个不同的值iii和jjj,并交换元素pip_ipi和pjp_jpj。
应用上述操作一次,求f(p)f(p)f(p)的最大值。
长度为nnn的排列†A†A†A是指由nnn个不同的整数1−n1-n1−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]不是一个排列(222在数组中出现两次),[1,3,4][1,3,4][1,3,4]也不是一个排列(n=3n=3n=3,但数组中有444)。
分析:
一个点是好数的条件就是ai=ia_i=iai=i,且小于其的数都在其左侧,大于其的数都在其右侧。
首先如果已经是有序排列了,那么答案是n−2n-2n−2,需要特判这种情况。
对于本题,执行把一个小的数交换到大的数后面去的操作,这不会使f(p)f(p)f(p)变得更大,所以不会执行。同理,把一个大的数放到后面去不会使f(p)f(p)f(p)变得更小,所以最初是好数的点操作完仍然还是好数。
本题只能交换一次,其实可以发现选择是有限的。我们会进行的操作,其结果只有两种可能
- 操作ai,aa_i,aai,a,会让ai,aa_i,aai,a中间的某个位置变成好数
- 操作ai,aa_i,aai,a,会让ai,aa_i,aai,a其中某个点本身变成好数
对于某个点,如果已经满足ai=ia_i=iai=i,我们看一下它的前面有多少个数大于它,如果没有,那么已经满足要求,如果有一个,可以找到需要交换的位置,记录下来,否则不可能在一次操作内让这个点变成好数,忽略即可。
所以,可能对f(p)f(p)f(p)产生贡献的位置对,只有2∗n2*n2∗n个,暴力检查这2∗n2*n2∗n个,算出贡献改变量即可。
统计一下序列中已经是好数的位置,计这个个数是cntcntcnt
-
如果所有位置都归位,那么交换会损失两个,也就是cnt−2cnt-2cnt−2
-
至少有一个没归位的。如果交换一个归位的和一个没归位的,相邻就会只损失一个位置,答案至少为cnt−1cnt-1cnt−1。在本题中,没归位的是成对出现的,并且一定存在没归位的两个位置是逆序的(假设都是正序的,那么就都归位了,矛盾)。交换这两个逆序位置,cntcntcnt不变,所以答案可以取到cntcntcnt
-
当交换(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]的位置不会受影响。由前文分析得px>pyp_x>p_ypx>py一定成立。考虑[x+1,y−1][x+1,y-1][x+1,y−1]这个区间里的位置iii,在(x,y)(x,y)(x,y)交换前,是没有好数的,因为若将(i,pi)(i,p_i)(i,pi)看成是二维平面点的时候,左侧的点都在左下方,右侧的点都在右上方,也就意味着,当iii左侧的值和iii右侧的值没有形成逆序对时,才会出现好数,而pxp_xpx和pyp_ypy是一对逆序对,所以没有好数位置。
枚举所有可能增加的位置对,将对应贡献加上,共有2∗n2*n2∗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,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。

2842

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



