Hello 2024 题解
本文网址:https://www.cnblogs.com/zsc985246/p/17950558 ,转载请注明出处。
虽然已经退役了,但还是保持着打 CF 的习惯。
比赛开始时 15 分钟网页非常卡,导致 BC 两题吃了很多罚时和提交时间。最终排名 1166。
题目很有趣,推荐。(这不比隔壁 Goodbye 2023 好 114514 倍)
2024/1/19update:更新 F1、F2 题解。
E、G、H 题解请等待后续更新。
传送门
A.Wallet Exchange
题目大意
Alice 和 Bob 玩游戏,Alice 先手。
两人各有一个数字 \(a,b\)。每个人需要依次进行下面两个操作:
-
交换两个人的数字或什么都不做。
-
将自己当前的数字减 \(1\)。
如果自己的数字小于 \(0\) 则输。求最后谁会赢。
多组测试,\(1 \le a,b \le 10^9, T \le 1000\)。
思路
因为如果任意一方的数字不为 \(0\),我们就可以将这个数字换给自己,所以游戏结束时两人的数字一定均为 \(0\)。
判断两人初始数字和的奇偶性即可。
代码实现
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
using namespace std;
ll n,m;
void mian(){
scanf("%lld%lld",&n,&m);
if((n+m)&1)printf("Alice\n");
else printf("Bob\n");
}
int main(){
int T=1;
scanf("%d",&T);
while(T--)mian();
return 0;
}
B.Plus-Minus Split
题目大意
给定一个长度为 \(n\) 的序列 \(a\),你需要将它划分成若干个区间。区间的权值为其中所有数的和的绝对值。
求所有区间权值和的最小值。
多组测试,\(1 \le n \le 5000, T \le 1000\)。
思路
显然不划分就是最优的。直接计算答案即可。
代码实现
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
using namespace std;
ll n,m;
void mian(){
ll ans=0;
scanf("%lld",&n);
while(getchar()!='\n');
For(i,1,n){
if(getchar()=='+')++ans;
else --ans;
}
printf("%lld\n",abs(ans));
}
int main(){
int T=1;
scanf("%d",&T);
while(T--)mian();
return 0;
}
C.Grouping Increases
题目大意
给定一个长度为 \(n\) 的序列 \(a\)。你有两个空序列 \(s,t\)。
你需要依次操作每个序列 \(a\) 中的数。你可以选择将它加入 \(s\),也可以选择将它加入 \(t\)。
假设 \(s,t\) 的长度分别为 \(p,q\)。定义权值 \(val=\sum\limits_{i=1}^{p-1}[s_i<s_{i+1}]+\sum\limits_{i=1}^{q-1}[t_i<t_{i+1}]\)。
求权值 \(val\) 的最小值。
多组测试,\(1 \le n \le 2 \times 10^5, 1 \le a_i \le n, T \le 10^4, \sum n \le 2 \times 10^5\)。
思路
贪心地考虑每个数。记 \(s,t\) 的最后一个数为 \(x,y\),初始时 \(x,y\) 为极大值。
定义一个数能加入序列,当且仅当这个数不大于序列的最后一个数。
考虑加入数 \(z\)。
-
如果 \(z\) 能加入 \(s\) 也能加入 \(t\):将 \(z\) 加入 \(x,y\) 中较小值代表的集合。
-
如果 \(z\) 只能加入 \(s\) 或只能加入 \(t\):将 \(z\) 加入其能加入的集合。
-
如果 \(z\) 不能加入 \(s\)也不能加入 \(t\):将 \(z\) 加入 \(x,y\) 中较小值代表的集合。
代码实现
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=1e6+10;
using namespace std;
ll n,m;
ll a[N],b[N];
void mian(){
ll ans=0;
ll t1=1e9,t2=1e9;
scanf("%lld",&n);
For(i,1,n){
scanf("%lld",&a[i]);
if(t1>=a[i]&&t2>=a[i]){
if(t1>t2)t2=a[i];
else t1=a[i];
}else if(t1>=a[i])t1=a[i];
else if(t2>=a[i])t2=a[i];
else if(t1<t2)t1=a[i],++ans;
else t2=a[i],++ans;
}
printf("%lld\n",ans);
}
int main(){
int T=1;
scanf("%d",&T);
while(T--)mian();
return 0;
}
D.01 Tree
题目大意
一棵树是好树当且仅当其满足以下条件:
-
所有非叶子节点都有两个儿子。
-
所有非叶子节点连向左右儿子的边中,一条边权值为 \(0\),一条边权值为 \(1\)。
定义树上两点的距离为它们的路径上所有边权的和。
将一棵好树的所有叶子节点按 dfs 序依次标号为 \(1 \to n\)。
给定一个长度为 \(n\) 的序列 \(a\),求是否存在一棵好树,满足 \(i\) 号节点到根节点的距离为 \(a_i\)。存在输出 Yes
,否则输出 No
。
多组测试,\(2 \le n \le 2 \times 10^5, 0 \le a_i \le n-1, T \le 10^4, \sum n \le 2 \times 10^5\)。
思路
发现一个合法的序列一定有且仅有一个 \(0\),且 \(x(x>0)\) 在序列中时,\(x-1\) 必定在序列中。
由于边权只有 \(0,1\),所以如果我们需要在序列中插入一个数 \(x\),那么一定只能在 \(x-1\) 的左边或右边。
直接判断给定的序列是否满足条件即可。
我们使用 dfs。
假设当前的数字为 \(x\),区间为 \([l,r]\)。
用 vector 记录 \(x+1\) 出现的位置,二分找到所有在 \([l,r]\) 内的 \(x+1\),将 \([l,r]\) 分隔为一些小区间,递归处理。
如果到达一个长度为 \(1\) 的区间且这个数不为 \(x+1\) 则输出 No
。
代码实现
注意特判 \(0\) 的个数。
如果使用笛卡尔树或者精细实现的 bfs 可以做到 \(O(n)\)。
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
#define pb push_back
const ll N=1e6+10;
using namespace std;
ll n,m,flag;
ll a[N],b[N];
vector<ll>t[N];
void dfs(ll l,ll r,ll x){
if(l>r)return;
//二分
ll lx=lower_bound(t[x].begin(),t[x].end(),l)-t[x].begin();
ll rx=upper_bound(t[x].begin(),t[x].end(),r)-t[x].begin()-1;
if(rx-lx+1==0)return void(flag=1);//区间中的数不为x+1
//拆分小区间,递归
for(ll i=lx;i<=rx;++i){
dfs(l,t[x][i]-1,x+1);
l=t[x][i]+1;
}
dfs(l,r,x+1);
}
void mian(){
scanf("%lld",&n);
//预处理
flag=0;
For(i,0,n-1)t[i].clear();
For(i,1,n){
scanf("%lld",&a[i]);
t[a[i]].pb(i);
}
if(t[0].size()!=1){//特判
printf("No\n");
return;
}
For(i,0,n-1)t[i].pb((ll)1e9);//防止二分越界
dfs(1,n,0);
if(flag){
printf("No\n");
return;
}
printf("Yes\n");
}
int main(){
int T=1;
scanf("%d",&T);
while(T--)mian();
return 0;
}
E.Counting Prefixes
题目大意
思路
代码实现
F1.Wine Factory (Easy Version)
题目大意
与 F2 不同之处已标红。
现在有 \(n\) 座塔,第 \(i\) 座塔初始有 \(a_i\) 升水和一个能喝 \(b_i\) 升水的人。第 \(i\) 座塔与第 \(i+1\) 座塔之间有能通过 \(c_i\) 升水的管道。
接下来依次对 \(i \in [1,n]\):
- 第 \(i\) 座塔的人喝掉尽可能多的水(不超过 \(b_i\))。
- 如果 \(i \neq n\),最多 \(c_i\) 升水流向第 \(i+1\) 座塔。
定义答案为所有人喝掉的水量之和。
有 \(q\) 次修改,每次给定 \(t,x,y,z\),表示修改 \(a_t=x,b_t=y,c_t=z\)。你需要输出每次更新后的答案。
\(2 \le n \le 5 \times 10^5, 1 \le q \le 5 \times 10^5, 0 \le a_i,b_i,x,y \le 10^9, \color{red}{c_i=z=10^{18}}, 1 \le t \le n\)。
思路
仅适用 F1。
发现由于 \(c_i=10^{18}\),所有没喝完的水都可以流到下一座塔中。
考虑计算没有被喝掉的水量。
定义 \(d_i=a_i-b_i\),表示 \(i\) 流入 \(i+1\) 的水量,如果 \(d_i>0\),这些水需要剩下的人喝掉。
那么整个后缀的贡献为 \(res_i=\sum\limits_{j=i}^{n}d_j\),而答案则为 \(\sum\limits_{i=1}^{n}a_i-\max\limits_{i=1}^{n}(ans_i)\)。
使用线段树维护即可,复杂度 \(O((n+q) \log n)\)。
代码实现
注意数组大小。
常数有点大。
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=2e6+10;
using namespace std;
ll n,m,k,q;
ll a[N],b[N],c[N];
ll d[N];
//线段树
#define lson rt<<1
#define rson rt<<1|1
ll tr[N],lazy[N];
void pushdown(ll rt){
if(lazy[rt]){
lazy[lson]+=lazy[rt];
tr[lson]+=lazy[rt];
lazy[rson]+=lazy[rt];
tr[rson]+=lazy[rt];
lazy[rt]=0;
}
}
void change(ll rt,ll l,ll r,ll x,ll y,ll z){
if(x<=l&&r<=y){
tr[rt]+=z;
lazy[rt]+=z;
return;
}
pushdown(rt);
ll mid=(l+r)>>1;
if(x<=mid)change(lson,l,mid,x,y,z);
if(y>mid)change(rson,mid+1,r,x,y,z);
tr[rt]=max(tr[lson],tr[rson]);
}
void mian(){
ll ans=0,sum=0;
scanf("%lld%lld",&n,&q);
For(i,1,n)scanf("%lld",&a[i]);
For(i,1,n)scanf("%lld",&b[i]);
For(i,1,n-1)scanf("%lld",&c[i]);
Rep(i,n,1){
d[i]=a[i]-b[i];
sum+=d[i];//后缀和
ans+=a[i];
change(1,1,n,i,i,sum);
}
while(q--){
ll t,x,y,z;
scanf("%lld%lld%lld%lld",&t,&x,&y,&z);
ans+=x-a[t];
change(1,1,n,1,t,x-y-d[t]);
a[t]=x,b[t]=y,c[t]=z,d[t]=x-y;
printf("%lld\n",ans-max(0ll,tr[1]));//可能出现负数,需要对0取max
}
}
int main(){
int T=1;
while(T--)mian();
return 0;
}
F2.Wine Factory (Hard Version)
题目大意
与 F1 不同之处已标红。
现在有 \(n\) 座塔,第 \(i\) 座塔初始有 \(a_i\) 升水和一个能喝 \(b_i\) 升水的人。第 \(i\) 座塔与第 \(i+1\) 座塔之间有能通过 \(c_i\) 升水的管道。
接下来依次对 \(i \in [1,n]\):
- 第 \(i\) 座塔的人喝掉尽可能多的水(不超过 \(b_i\))。
- 如果 \(i \neq n\),最多 \(c_i\) 升水流向第 \(i+1\) 座塔。
定义答案为所有人喝掉的水量之和。
有 \(q\) 次修改,每次给定 \(t,x,y,z\),表示修改 \(a_t=x,b_t=y,c_t=z\)。你需要输出每次更新后的答案。
\(2 \le n \le 5 \times 10^5, 1 \le q \le 5 \times 10^5, 0 \le a_i,b_i,x,y \le 10^9, \color{red}{0 \le c_i,z \le 10^{18}}, 1 \le t \le n\)。
思路
发现 F1 的做法并不适用,也不好进行更多拓展。
我们仍然使用线段树,考虑将一段区间的塔看作一个塔,记录它能向后流多少水、还能喝掉多少水、管道还能装多少水、总共喝了多少水。
合并区间依次考虑过程即可,具体实现见代码。
代码实现
注意细节。
常数有点大。
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=2e6+10;
using namespace std;
ll n,m,k,q;
ll a[N],b[N],c[N];
//线段树
#define lson rt<<1
#define rson rt<<1|1
struct node{
ll flow;//能向后流多少水
ll tmp;//还能喝掉多少水
ll pipe;//管道还能装多少水
ll res;//总共喝了多少水
}tr[N];
node operator+(node a,node b){
ll t=min(a.flow,b.tmp);//喝了多少水
node c;
ll t1=min(a.flow-t,b.pipe);//剩下流出的水
ll t2=min(b.tmp-t,a.pipe);//b还能喝多少水
c.flow=b.flow+t1;
c.tmp=a.tmp+t2;
c.pipe=min(a.pipe-t2,b.pipe-t1);//需要减去t2是因为这些水会在b处被喝掉,它们进了a的管道,但不进b的管道
c.res=a.res+b.res+t;
return c;
}
void change(ll rt,ll l,ll r,ll x){
if(l==r){
ll t=min(a[l],b[l]);//喝了多少水
ll t1=min(c[l],a[l]-t);//剩下流出的水
tr[rt]={t1,b[l]-t,c[l]-t1,t};
return;
}
ll mid=(l+r)>>1;
if(x<=mid)change(lson,l,mid,x);
else change(rson,mid+1,r,x);
tr[rt]=tr[lson]+tr[rson];
}
void mian(){
ll ans=0;
scanf("%lld%lld",&n,&q);
For(i,1,n)scanf("%lld",&a[i]);
For(i,1,n)scanf("%lld",&b[i]);
For(i,1,n-1)scanf("%lld",&c[i]);
For(i,1,n)change(1,1,n,i);
while(q--){
ll t,x,y,z;
scanf("%lld%lld%lld%lld",&t,&x,&y,&z);
a[t]=x,b[t]=y,c[t]=z;
change(1,1,n,t);
printf("%lld\n",tr[1].res);
}
}
int main(){
int T=1;
while(T--)mian();
return 0;
}
尾声
如果有什么问题,可以直接评论!
都看到这里了,不妨点个赞吧!