Hello 2024 题解

本文网址:https://www.cnblogs.com/zsc985246/p/17950558 ,转载请注明出处。

虽然已经退役了,但还是保持着打 CF 的习惯。

比赛开始时 15 分钟网页非常卡,导致 BC 两题吃了很多罚时和提交时间。最终排名 1166。

题目很有趣,推荐。(这不比隔壁 Goodbye 2023 好 114514 倍)

2024/1/19update:更新 F1、F2 题解。

E、G、H 题解请等待后续更新

传送门

Hello 2024

A.Wallet Exchange

题目大意

Alice 和 Bob 玩游戏,Alice 先手。

两人各有一个数字 \(a,b\)。每个人需要依次进行下面两个操作:

  1. 交换两个人的数字或什么都不做。

  2. 将自己当前的数字减 \(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\)

  1. 如果 \(z\) 能加入 \(s\) 也能加入 \(t\):将 \(z\) 加入 \(x,y\) 中较小值代表的集合。

  2. 如果 \(z\) 只能加入 \(s\) 或只能加入 \(t\):将 \(z\) 加入其能加入的集合。

  3. 如果 \(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

题目大意

一棵树是好树当且仅当其满足以下条件:

  1. 所有非叶子节点都有两个儿子。

  2. 所有非叶子节点连向左右儿子的边中,一条边权值为 \(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]\)

  1. \(i\) 座塔的人喝掉尽可能多的水(不超过 \(b_i\))。
  2. 如果 \(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]\)

  1. \(i\) 座塔的人喝掉尽可能多的水(不超过 \(b_i\))。
  2. 如果 \(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;
}

尾声

如果有什么问题,可以直接评论!

都看到这里了,不妨点个赞吧!

posted @ 2024-01-07 15:53  zsc985246  阅读(268)  评论(0编辑  收藏  举报