2023 牛客暑期多校简单题解

 A.Almost Correct

待补

B.Anticomplementary Triangle

待补

C.Carrot Trees

题意:给定一个长度为n的数组初始值均为0,给定一个常量k。定义以下两个区间操作

1 l r x  区间[l,r]加x/k

2 l r     区间[l,r]之间大于1的位置-1

求所有操作后 减的操作执行次数。

思路:一眼线段树,但是不知道怎么处理实数和记录等于0的位置。看了题解,借位的思路很妙,

首先如何去处理实数?因为k在所有操作中是一样的 所以我们可以将操作转化为区间加 x ,和区间内大于等于k的值 -k,这样就可以在整数里面操作了。

那么如何去记录所有位置减的次数?记录区间内小于k的数量,如果都是0则不作操作,如果都大于等于k则记录,这些显然会爆时间。题解使用了借位的思想(美丽的数学)

设 a_i=c_i+k*b_i,ai 为位置i真正的值,ci为解除0的限制的条件下线段树进行各个操作后的值,bi为将ci转变为ai所需要的倍数,也就是重新加上0这个限制条件。

考虑这样一个过程,对于操作1 ,我们之间加上x值就可以了,也就是 a_{i+1}=a_i+x=c_i+x+b_i*k

那么对于操作2 则要分情况 

情况1 

 a_i\geqslant k 也就是c_i-k\geqslant -b_i*k 这种情况下我们之间 c_{i+1}=c_i-k,b_{i+1}=b_i

c_i本身的值已经足够去进行一次 -k 的操作,直接减去就好了,b_i则是继承上一回的值。

情况2

a_i<k 也就是 c_i-k< -b_i*k 

c_i本身的值不足以支持-k的操作,如果直接减去那么在0的限制下 a_i=c_i+k*b_i这个等式就不成立了,所以我们要先向bi借一位来保证等式成立。所以c_{i+1}=c_i-k , b_{i+1}=b_i+1

可以看出 b是一个递增的变量,且这两者之间又某种神奇的关系。看了过程可以猜到 

b_{max}=\lceil -\frac{c_{min}}{k} \rceil

题解的证明十分清晰

所以我们只需要维护每个位置c的历史最小值即可。

//区间修改 单点查询历史最小值 
#include
#include
#include
#include
#include
#include
#include
#define fi first
#define se second
#define ll long long
#define ull unsigned long long
using namespace std;
const int N=1e6+5;
const ll md=1e9+7;
const ll inf=1e18;
const ll eps=1e-9;
const double E=2.718281828;
struct linetree{
	ll lazy,min_lazy;//区间修改标签 区间修改历史最小标签 
	ll val,min_val;//区间最小值 区间历史最小值 
}tr[N<<2];
ll n,m,k;
void up(int rt){
	tr[rt].val=min(tr[rt<<1].val,tr[rt<<1|1].val);
	tr[rt].min_val=min(tr[rt<<1].min_val,tr[rt<<1|1].min_val);
	//和普通线段树一样正常上推信息 
}
void down(int rt){
	int ls=rt<<1,rs=rt<<1|1;
	//历史最值线段树比普通线段树多一个历史标签,用来记录父亲操作的一个前缀最小或者最大 
	//然后根据现有的最小值和父亲下传的最小操作前缀更新历史最小值
	
	//左儿子 
	tr[ls].min_lazy=min(tr[ls].min_lazy,tr[ls].lazy+tr[rt].min_lazy);
	tr[ls].min_val=min(tr[ls].min_val,tr[ls].val+tr[rt].min_lazy);
	tr[ls].lazy+=tr[rt].lazy;
	tr[ls].val+=tr[rt].lazy;
	//右儿子 
	tr[rs].min_lazy=min(tr[rs].min_lazy,tr[rs].lazy+tr[rt].min_lazy);
	tr[rs].min_val=min(tr[rs].min_val,tr[rs].val+tr[rt].min_lazy);
	tr[rs].lazy+=tr[rt].lazy;
	tr[rs].val+=tr[rt].lazy;
	
	tr[rt].lazy=tr[rt].min_lazy=0;//标签清空 
}
//void build(int l,int r,int rt){
//	if(l==r){
//		
//	}
//}
void updata(int l,int r,int rt,int pl,int pr,ll v){
	if(pl<=l&&r<=pr){
		tr[rt].min_lazy=min(tr[rt].min_lazy,tr[rt].lazy+v);
		tr[rt].min_val=min(tr[rt].min_val,tr[rt].val+v);
		tr[rt].lazy+=v;
		tr[rt].val+=v;
		return ;
	}
	down(rt);
	int m=l+r>>1;
	if(pl<=m){
		updata(l,m,rt<<1,pl,pr,v);
	}
	if(pr>m){
		updata(m+1,r,rt<<1|1,pl,pr,v); 
	}
	up(rt);
}
ll queryr(int l,int r,int rt,int x){
//	cout<<"query r "<>1;
	down(rt);
	if(x<=m){
		return queryr(l,m,rt<<1,x);
	}else{
		return queryr(m+1,r,rt<<1|1,x);
	}
}
ll query(int l,int r,int rt,int x){
	if(l==r){
		return tr[rt].min_val;
	}
	int m=l+r>>1;
	down(rt);
	if(x<=m){
		return query(l,m,rt<<1,x);
	}else{
		return query(m+1,r,rt<<1|1,x);
	}
}
void pt(){
	vectornum,num1;
	for(int i=1;i<=n;i++){
		num.push_back(queryr(1,n,1,i));
		num1.push_back(query(1,n,1,i));
	}
	for(int v:num){
		cout<>n>>m>>k;
	int op,l,r,x;
//	build(1,n,1);
//初始值为0就懒得写build了 坏习惯 
	ll ans=0;
	while(m--){
		cin>>op>>l>>r;
		if(op==1){//区间加  x
			cin>>x;
			updata(1,n,1,l,r,x);
//			pt();
		}else{// 区间减 k
			ans+=(ll)(r-l+1);//有借位存在 所以我们可以先假定无0限制所有位置均可以减 
			updata(1,n,1,l,r,-k);
//			pt();
		}
	}
	for(int i=1;i<=n;i++){
		ll c=query(1,n,1,i);
//		cout<>t;
	while(t--){
		solve();
	}

}

D.Chocolate

题意:有一块n*m的巧克力,每次玩家可以吃(1,1)到(i,j)这个矩形之间的巧克力,每个玩家在各自回合必须吃至少一个单位的巧克力,吃掉最后一块巧克力的人输掉比赛。

(从皇家翻译获得题意后反手喂给队里的博弈选手,就秒了?)

思路:先手直接将巧克力吃成一个L形,可以根据后手的操作去调整自己的操作(对称博弈?)所以只有巧克力为1*1的尺寸的时候先手没有办法去进行对称所以 后手赢

#include
#include
#include
#include
#include
#include
#include
#define fi first
#define se second
#define ll long long
#define ull unsigned long long
using namespace std;
const int N=0;
const ll md=1e9+7;
const ll inf=1e18;
ll n,m;
void solve(){
	//fucking and strange
	cin>>n>>m;
	if(n==1&&m==1){
        cout<<"Walk Alone"<>t;
	while(t--){
		solve();
	}
}

H.Matches

题意:给定两个数组 a b 长度为n 可以在任意数组内进行至多一次交换 定义d=\sum \left | a_i-b_i \right |

求最小的d

思路:一股浓浓的CF的味道,队友写了分类讨论有道理但是很麻烦。对于每一对 a_ib_i我们可以看做一条线段 d就可以看作所有线段的长度和。然后去最小化d,如果不进行任何操作,d就是初始线段所有的长度。如果线段与线段之间由相交的区域,那么我们就可以减去这个区域*2的长度。

所以我们找到在合法交换下可以消除的最大相交区域。

根据a_ib_i的关系进行分类

a_i\geq b_i  标记为a为右端点 b为左端点

a_i<b_i  标记为b为右端点 a为左端点

所有线段按照右端点进行由大到小排序,对于每个类维护后缀最小值(最远的左端点)

然后就可以枚举a b,进行快乐的二分了

#include
#include
#include
#include
#include
#include
#include
#define fi l
#define se r
#define ll long long
#define ull unsigned long long
using namespace std;
const int N=1e6+5;
const ll md=1e9+7;
const ll inf=1e18;
ll n,a[N],b[N];
struct aa{
	ll l,r;
};
bool cmp(aa x,aa y){
	return x.ral,ar,bl,br;
ll sufal[N],sufar[N],sufbl[N],sufbr[N],all;
void solve(){
	//fucking and strange
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		cin>>b[i];
	}
	
	for(int i=1;i<=n;i++){
		if(a[i]<=b[i]){
			al.push_back({a[i],b[i]});
			br.push_back({a[i],b[i]});
		}else{
			ar.push_back({b[i],a[i]});
			bl.push_back({b[i],a[i]});
		}
		all+=abs(a[i]-b[i]);
	}
	sort(al.begin(),al.end(),cmp);
	sort(ar.begin(),ar.end(),cmp);
	sort(bl.begin(),bl.end(),cmp);
	sort(br.begin(),br.end(),cmp);//这里写的比较冗余,赛时就怎么清楚怎么来了
	ll del=0;
	sufal[al.size()]=sufbl[bl.size()]=inf;
	for(int i=al.size()-1;i>=0;i--){
		sufal[i]=min(sufal[i+1],al[i].fi);
	}
	for(int i=bl.size()-1;i>=0;i--){
		sufbl[i]=min(sufbl[i+1],bl[i].fi);
	}
	for(int i=0;i=key){
			del=max(del,min(ar[i].se-ar[i].fi,ar[i].se-sufal[l]));
		}
	}
	for(int i=0;i=key){
			del=max(del,min(br[i].se-br[i].fi,br[i].se-sufbl[l]));
		}
	}
//	cout<<"del "<>t;
	while(t--){
		solve();
	}

}

J.Roulette

题意:有一整数n,每次赌博可以支付 x_i的代价 有\frac{1}{2}的概率赢获得2x_i\frac{1}{2}的概率输损失x_i

x_1=1

如果第 i-1回合是获胜的那么x_i=1 否则 x_i=2*x_{i-1}

求多赚m元的概率

思路:手玩一下就可以发现每个n到n+1的概率是独立的,内部的概率计算是一个等比数列的和。

#include
#include
#include
#include
#include
#include
#include
#define fi first
#define se second
#define ll long long
#define ull unsigned long long
using namespace std;
const int N=0;
const ll md=998244353;
const ll inf=1e18;
ll n,m;
ll ksm(ll a,ll b){
	ll res=1;
	while(b){
		if(b&1)res=res*a%md;
		a=a*a%md;
		b>>=1;
	}
	return res;
}
ll getinv(ll a,ll b){
	return ksm(a,b-2);
}
ll getv(ll a1,ll q,ll n){//等比 
	return a1*((1+md-ksm(q,n))%md)%md* getinv((1+md-q)%md,md)%md;
}
void solve(){
	//fucking and strange
	cin>>n>>m;
	ll inv=getinv(2,md);
	ll now=0,be=n,win=1;
	for(int i=0;i<60;i++){
		now+=(1ll<=n+m){
			now=n+m;
		}
		if(now>=be){
//			cout<

K.Subdivision

题意:给定带有n个顶点和m条边的图,可以进行多次一下操作

选择一条边(u,v) ,由(u,w)(w,v)去替换它,w为新加的点

找出与顶点1距离不超过k的最大顶点数

思路:与顶点1的最小距离,可以通过bfs从图重构出各个点与顶点最小的树。那么只要去填满深度小于等于k的位置就可以,每个点可以延申出多少条链去填充通过维护每个点的deg来判断。

注意1和叶子节点的特判.

#include
#include
#include
#include
#include
#include
#include
#define fi first
#define se second
#define ll long long
#define ull unsigned long long
using namespace std;
const int N=1e5+5;
const ll md=998244353;
const ll inf=1e18;
int n,m,hd[N],cnte=0;
struct aa{
	int v,nt;
}e[N<<2];
void add(int v,int u){
	e[++cnte].v=u;
	e[cnte].nt=hd[v];
	hd[v]=cnte;
}
vectorson[N]; 
int vis[N],p[N];
ll k,deg[N],dep[N];
void bfs(int x){
	queueq;
	q.push(x);
	dep[x]=0;
//	cout<<"bfs "<>n>>m>>k;
	for(int v,u,i=1;i<=m;i++){
		cin>>v>>u;
		add(v,u);
		add(u,v);
		deg[v]++;
		deg[u]++;
	}
	bfs(1);
//	for(int i=1;i<=n;i++){
//		cout<>t;
	while(t--){
		solve();
	}
}

L.Three Permutations

中国剩余定理 待补

M.Water

题意:有A容量的杯子,B容量的杯子,确定能否通过以下操作确切的喝到x单位的水,如果能输出最小的操作数,否则输出-1

操作1:将两个瓶子中的一个装满水

操作2:将两个瓶子中的一个倒空

操作3:喝掉其中一个瓶子中的所有水

操作4:在不溢出的情况下,尽可能多地将水从一个瓶子转移到另一个瓶子。具体来说,如果两个瓶子分别含有a和b单位的水,他只能从A瓶向B瓶转移min(a,B−b)的水量,或者从B瓶向A瓶转移min(b,A−a)的水量。

题解:exgcd (赛时不会只能看着队友debug, 恼羞成怒,隔天去了解)

题解的证明,,,看不明白。

但是可以从题意里抽象出一个Ax+By=C 的一个式子,我们的目的之一是求出这个式子。这个很简单,套一个exgcd的板子就可以。接下来就是去将得到的x y 转化成操作数量。

如果x为正数 (我觉得)可以看作 往杯子里到入新的水或者喝掉某个杯子里面的水。

如果x为负数   (我觉得)可以看作 倒空某个杯子里的水或 转移水

y同上

那么当x\geq 0y\geq 0时 ans=2*(x+y) ,倒入新的水与喝水是两部操作故而 *2 下一种情况类似。

当 x*y<0 时 ans=2*abs(x-y)-1 ,因为最后有容器的水我可以不倒掉,所以节约了一次操作 故而 -1。

#include
#include
#include
#include
#include
#include
#include
#define fi first
#define se second
#define ll long long
#define ull unsigned long long
using namespace std;
const int N=0;
const ll md=1e9+7;
const ll inf=1e18;
const ll eps=1e-9;
const double E=2.718281828;
ll A,B,C;
ll exgcd(ll a,ll b,ll &x,ll &y){
	if(!b){
		x=1;
		y=0;
		return a;
	}
	ll g=exgcd(b,a%b,x,y);
	ll tmp=x;
	x=y;
	y=tmp-a/b*y;
	return g;
}
ll getv(ll p,ll kx,ll x,ll ky,ll y){
	x=kx*p+x;
	y=ky*p+y;
	if(x<0&&y<0){
		return inf;
	}
	if(x*y>=0){
		return 2*(x+y);
	}else{
		return 2*(abs(x)+abs(y))-1;
	}
}
void solve(){
	//fucking and strange
	cin>>A>>B>>C;
	ll x,y;
	ll g=exgcd(A,B,x,y);
	if(C%g){
		cout<<-1<<"\n";
	}else{
		ll mul=C/g;
		x*=mul;
		y*=mul;
		ll kx=B/g,ky=A/g*-1;
		ll p[4];
		p[1]=ceil(-1.0*x/kx),p[2]=floor(-1.0*y/ky);
		ll ans=inf;
		for(int i=1;i<=2;i++){
			for(int j=-1;j<=1;j++){
				ans=min(ans,getv(p[i]+j,kx,x,ky,y));
			}
		}
		cout<>t;
	while(t--){
		solve();
	}

}

你可能感兴趣的:(算法)