191105-瑶族CSP考试题T1~T2

得分: 85 \color{#30f000}85 85 + 0 \color{#d00000}0 0 + 20 \color{#f05000}20 20 = 105 \color{#f0a000}105 105

T1 盘王节

题目描述

盘王节是祭祀瑶族祖先盘古王的重大节日。

小 L 望着眼前的小 Q 穿着盛装, 鼓声镲声交错耳边, 远古而悠长的音节从小 Q 的口中淌出, 仿佛构筑起一条现世与始祖神灵的通路. 身旁的舞龙队伍盛大地游行着, 小Q 的一双手突然拉住了小 L, 他们融进了欢庆的人群中兴奋地共舞, 夜空之下烟火绽开,
华光渲染了他俩的眼眸, 更照亮了少年内心暗暗涌动的情愫…

节日庆典上, 小 Q 向小 L 介绍了一种特色双人益智游戏, 以下为游戏的大体内容:

  1. 双方分为进攻方和防守方。
  2. 进攻方有 n n n 种不同的符咒,第 i i i 种能力值为 a i a_i ai ,各有 x i x_i xi 个。
  3. 防守方有 m 1 m_1 m1 种不同的先灵庇佑的御符,第 i i i 种能力值为 b i b_i bi ,各有 y i y_i yi 个。
  4. 防守方还有 m 2 m_2 m2 种不同的兵符,第 i i i 种能力值为 c i c_i ci ,各有 z i z_i zi 个。

这个游戏由攻方主动,每次可以选择一个符击破守方的任意一个御符或任意一个兵符。

  1. 如果你的符的能力值不低于对方,则会破除对方这个符咒。
  2. 自己的符咒每一个只能用一次,不可收回
  3. 如果自己的符能力值低于对方,则符咒无效
  4. 进攻对方两种符的效果不同。
    1. 若进攻御符,会对对方造成0点伤害。当对方所有的御符全都被破除后,自己剩下的符可以越过对方兵符直接对对方造成符咒能力值的伤害。
    2. 若进攻兵符,会对对方造成两者能力值差点数的伤害。

问你最大能打出多少点伤害。

输入格式

第一行三个整数, 表示 n n n, m 1 m_1 m1, m 2 m_2 m2
接下来 n n n 行,每行分别给出 a i a_i ai, x i x_i xi
接下来 m 1 m_1 m1 行,一行分别给出 b i b_i bi, y i y_i yi
接下来 m 2 m_2 m2 行,一行分别给出 c i c_i ci, z i z_i zi

输出格式

只有一个整数,表示你的答案。

数据范围

在这里插入图片描述

解析

注意负数情况!!!

因为某一个符咒打了兵符,一定会产生一个非负数的收益;而如果那个符咒打了御符没打完的话,余下的情况一样,这里的收益为零。所以只有两种情况就是先打完御符或直接开始进攻兵符。排序

对于兵符,负数没有影响,因为只是做差就完了。为了得到更大的收益,试验了几种情况,发现了贪心策略:
从自己大的和对方小的同时枚举,以大打小,记录答案,直到没有收益为止。
对于御符,负数有一些影响,比如你有 10 张 -2 ,1 张 -1 的,对方有一个 -2 的御符,没有兵符,若你选择打掉御符,把所有符咒扔过去,自己白白吃一大堆负分。贪心策略:用自己和对方从小到大枚举,用自己不比对方小的符咒中尽量小的去进攻对方尽量小的符咒。最后你可以选择扔剩下的符咒,或者进攻兵符(如果你一个正数打中对方一个负数会比直接扔过去效果高),因此直接扔过去相当于对方新增了无限的能力值为 0 的符咒(不扔就等价于在零处销毁),再像正常一样打兵符打就行了,前提:你必须已经消灭对方所有御符。

最后两种情况去最大值即可。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+4;int n,m1,m2,ans1,ans2,finans,pos;
struct node{int x,y;}a[N],b[N],c[N],ls[N],rs[N];
inline bool cmp(node xx,node yy){return xx.x<yy.x;}
inline int Read(){
	register int x=0,f=1;register char ch=getchar();
	while(ch>=58||ch<=47){if(ch=='-')f=-1;ch=getchar();}
	while(ch<=57&&ch>=48){x=(x<<3)+(x<<1)+ch-48;ch=getchar();}
	return f*x;
}
signed main(){
	n=Read(),m1=Read(),m2=Read();
	for(register int i=1;i<=n;i++)a[i].x=Read(),a[i].y=Read();
	for(register int i=1;i<=m1;i++)b[i].x=Read(),b[i].y=Read();
	for(register int i=1;i<=m2;i++)c[i].x=Read(),c[i].y=Read();
	sort(a+1,a+n+1,cmp),sort(b+1,b+m1+1,cmp),sort(c+1,c+m2+1,cmp);//排序
	for(register int i=1;i<=n;i++)ls[i]=a[i];//复制几遍初值方便用
	register int bl=1,j=1;
	while(bl<=m1&&j<=n){//打光御符的情况自然跟牌法
		if(ls[j].x<b[bl].x)++j;
		else{
			if(ls[j].y>=b[bl].y)ls[j].y-=b[bl].y,b[bl].y=0,++bl;//打对方下一个御符
			else b[bl].y-=ls[j].y,ls[j].y=0,++j;//自己不够就用后继符
		}
	}
	if(bl>m1){//打光了才干这件事
		for(register int i=1;i<=m2;i++){rs[i]=c[i];if(rs[i].x<0)pos=i;}++pos;//找到零位
		for(int i=m2;i>=pos;i--)rs[i+1]=rs[i];
		rs[pos].x=0,rs[pos].y=666666666666666666,bl=1,j=n;//插入无穷个0
		while(j&&bl<=m2+1){//剩下的打兵符
			if(ls[j].x>=rs[bl].x){
				if(ls[j].y>=rs[bl].y)ans1+=(ls[j].x-rs[bl].x)*rs[bl].y,ls[j].y-=rs[bl].y,rs[bl].y=0,++bl;
				else ans1+=(ls[j].x-rs[bl].x)*ls[j].y,rs[bl].y-=ls[j].y,ls[j].y=0,--j;
			}
			else --j;
		}
	}
	bl=1,j=n;
	while(j&&bl<=m2){//直接打兵符大打小
		if(a[j].x>=c[bl].x){
			if(a[j].y>=c[bl].y)ans2+=(a[j].x-c[bl].x)*c[bl].y,a[j].y-=c[bl].y,c[bl].y=0,++bl;
			else ans2+=(a[j].x-c[bl].x)*a[j].y,c[bl].y-=a[j].y,a[j].y=0,--j;
		}
		else --j;
	}
	finans=max(ans1,ans2);printf("%lld",finans);//两种情形取最大
	return 0;
}

我就是没有考虑越过兵符得负分的情况,WA 了 15 分。时间复杂度 O ( n log ⁡ 2 n ) O(n \log_2n) O(nlog2n) ,因为多次排序(常数较大)最大的点不开氧要 接近 4000 4000 4000 m s ms ms ,开了可以AC。

T2 祝著节

瑶族村落共由 n n n 个村庄, m m m 条双向道路构成, 每条道路要么属于小 Q 的管辖范围, 我们称它为类型 A A A;要么属于现任族长小 J 的管辖范围, 我们称它为类型 B B B
定义一个道路集合是合法的,当且仅当其中至少包含一条类型 A A A 和一条类型 B B B 的道路, 并且任意两个村庄可以仅经过集合中的道路互相到达一个道路集合的权值为所含道路长度之和。( M S T MST MST
瑶族居民的幸运数字为 X X X ,因此最小的合法道路集合权值必须等于 X X X(当然,至少要存在一个合法的道路集合)。
小 L 手中的这张地图能依稀辨认出 n n n 个村庄和 m m m 条道路, 却无法分辨每条道路属于类型 A A A 还是类型 B B B 。他想知道有多少种可能的地图,满足瑶族村庄的分布规律。两种方案不同,当且仅当至少存在一条道路,在两种方案中类型不同。
由于答案很大,你需要把答案对 1 0 9 + 7 10^9+7 109+7 取模后输出。

输入格式

第一行一个正整数 T T T, 表示数据组数。
每组数据第一行三个整数 n n n, m m m, X X X 表示村庄个数,道路条数,以及瑶族居民的幸运数字。
接下来 m m m 行, 每行三个整数 x , y , z x,y,z x,y,z , 表示一条连接村庄 x x x 和村庄 y y y 的道路, 其长度为 z z z

输出格式

总共 T T T 行, 每行一个整数, 表示答案对 1 0 9 + 7 10^9+7 109+7 取模后的结果。

数据范围

在这里插入图片描述

解析

题目大意:问有多少种道路归属可以满足最小的合法道路集合权值和等于幸运数字。

如果没有两种颜色的限制,那么最小集合一定是最小生成树。若有 族 长 小 Q \color{#FF0000}族长小Q Q 族 长 小 J \color{#0000FF}族长小J J的管辖的限制,只能改动 x ≤ 1 x \le 1 x1 条边。证明:如果在MST改两条边及以上时,可以一条是 族 长 小 Q \color{#FF0000}族长小Q Q色,一条属于 族 长 小 J \color{#0000FF}族长小J J色,一定过度满足(一种颜色与剩下未改的一边相同),并且权值和增大了两次,可以少增加一次会更小(类似于不严格次小生成树)。

再说改边,首先Kruskal属于贪心算法,可以证明如果从MST集合里更换集合外一条边不会更小。再说变大幅度的问题,依次枚举的边是不能变的,要变大得少一点,要让改的边的权值尽量大(差值越小越好)。因此,我们可以在MST上跑LCA记录经过的最大权值的边。

最后计数,设 s u m sum sum 为最小生成树的权值和,分情况讨论:

  1. s u m = = X sum==X sum==X ,枚举换进来的边,设有 p r o b prob prob 条边满足权值和 a n s = X ans=X ans=X (也是MST),先确定原图MST方案那些边的颜色为 p p p ,然后从 p r o b prob prob 条边至少选一条与 p p p 不同颜色,剩下随便选,答案为 m m m 条边任选减去生成树 n − 1 n-1 n1 条边与换上的边同色其余任选的情况:

2 m − 2 m − ( n − 1 ) − p r o b 2^m-2^{m-(n-1)-prob} 2m2m(n1)prob

  1. s u m < X sum<X sum<X ,与上一种情况类似,在加上有 b a n ban ban 条边满足答案 a n s < X ans<X ans<X ,先定下原图MST方案那些边的颜色 p p p ,然后从 p r o b prob prob 条边中至少选一条与 p p p 颜色不同,且剩下的边中有 b a n ban ban 条边也要与 p p p 颜色相同(要不然一堆堆同色则不合法)。答案为负色(*2),除了 b a n ban ban 条边与MST同色,减去上述全部同色(如一片黄){没谈到的全部任选}:

2 ∗ ( 2 m − ( n − 1 ) − b a n − 2 m − ( n − 1 ) − b a n − p r o b ) 2*(2^{m-(n-1)-ban}-2^{m-(n-1)-ban-prob}) 2(2m(n1)ban2m(n1)banprob)

祝贺自己通过了一道富有文化底蕴的题。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,sum,poww[233333];//提前处理好2的次幂
struct E{int nxt,to,w;}edge[333333];//最小生成树的边
int tot,head[111111],f[111111][20];//lca用
int dep[111111],maxx[111111][20];//通往祖先路径最大的权值
void Merge(int x,int y,int z){//连边
	edge[++tot].to=y;edge[tot].nxt=head[x];
	edge[tot].w=z;head[x]=tot;
}
void Init(int u,int fat){//倍增LCA预处理
	dep[u]=dep[fat]+1;
	for(int i=0;i<=18;i++){
		f[u][i+1]=f[f[u][i]][i];
		maxx[u][i+1]=max(maxx[u][i],maxx[f[u][i]][i]);//从此点到2^k辈祖先路径的最大权值
	}
	for(int e=head[u];e;e=edge[e].nxt){
		int v=edge[e].to;if(v==fat)continue;
		f[v][0]=u;maxx[v][0]=edge[e].w;//到上一级的最大权值就是这一条边
		Init(v,u);//继续
	}
}
int Query(int x,int y){//询问从x到y路径最大权值
	int ans=0;
	if(dep[x]<dep[y])swap(x,y);
	for(int i=19;i>=0;i--){
		if(dep[f[x][i]]>=dep[y]){
			ans=max(ans,maxx[x][i]);x=f[x][i];//较深的上跳,处理出到祖先的路径最大权值
		}
	}
	if(x==y)return ans;//到了
	for(int i=19;i>=0;i--){
		if(f[x][i]!=f[y][i]){//一起上跳
			ans=max(maxx[x][i],ans);//从x到祖先的最大路径权值
			ans=max(maxx[y][i],ans);//从y到祖先的最大路径权值
			x=f[x][i],y=f[y][i];
		}
	}
	ans=max(ans,max(maxx[x][0],maxx[y][0]));//两边的最大值在和以前的比较
	return ans;
}
int vis[333333],x;//vis是使用了边构成MST
struct B{
	int a,b,c;//最小生成树用
}bian[333333];
int fa[333333];//并查集用
void iinit(){for(int i=0;i<=333332;i++)fa[i]=i;}//初始化
int gf(int x){//getfather
	if(fa[x]==x)return x;
	return fa[x]=gf(fa[x]);
}
bool cmp(B SS,B TT){return SS.c<TT.c;}
void Kruskal(){
	sort(bian+1,bian+m+1,cmp);
	int f1,f2;
	iinit();
	for(int i=1;i<=m;i++){
		f1=gf(bian[i].a),f2=gf(bian[i].b);
		if(f1!=f2){
			fa[f1]=f2;vis[i]=1;//记录边已经用过啦
			Merge(f1,f2,bian[i].c);
			Merge(f2,f1,bian[i].c);//用于LCA
			sum+=bian[i].c;//MST的权值之和
		}
	}
}
signed main(){
	int T;scanf("%lld",&T);poww[0]=1;
	for(int i=1;i<=233332;i++)poww[i]=poww[i-1]*2%1000000007;//预处理2^k次幂
	while(T--){
		memset(edge,0,sizeof(edge)),memset(head,0,sizeof(head));
		memset(dep,0,sizeof(dep)),memset(bian,0,sizeof(bian));
		memset(vis,0,sizeof(vis)),memset(f,0,sizeof(f));
		memset(maxx,0,sizeof(maxx)),memset(fa,0,sizeof(fa));tot=sum=0;//清零
		scanf("%lld%lld%lld",&n,&m,&x);
		for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&bian[i].a,&bian[i].b,&bian[i].c);
		Kruskal();//克鲁斯卡尔算法
		Init(1,0);//假定1位根
		int ban=0,prob=0;
		for(int i=1;i<=m;i++){//枚举不在MST的每一条边
			if(vis[i])continue;//已经在最小生成树时用过了
			int now=sum+bian[i].c-Query(bian[i].a,bian[i].b);//找到原来树上路径最大权值的边取下来,把这一条边换上去
			if(now==x)++prob;//变成了幸运数字记录prob变量
			if(now<x)++ban;//小于幸运数字记录ban变量
		}
		int res=0;
		if(sum==x)res=poww[m]-2ll*(poww[m-(n-1)-prob]);//计数1
		if(sum<x)res=2ll*(poww[m-(n-1)-ban]-poww[m-(n-1)-ban-prob]);//计数2
		res%=1000000007;if(res<0)res+=1000000007;//小心减出负数了
		printf("%lld\n",res);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值