IOI2020集训队作业-20 (CF603E, AGC036E, AGC021E)

A - CF603E Pastoral Oddities

Sol

考虑如何判断一张图是否可以通过删除若干条边变成sunny的:每个连通块显然是独立的;如果连通块中的点数是奇数显然不行(因为所有点的度数和为偶数,所以必然不可能所有的点的度数都是奇数),否则一定可以(随便搞一棵生成树出来,然后对非树边随意决定是否保留,对树dfs,每个点的度数由它到父亲的边调整为奇数,根节点无法调整但是由于度数和为偶数所以根节点的度数也一定是奇数)。

用lct维护 w w w的最小生成树(也就是维护连通性)。新加入一条边之后,检查能否删掉最小生成树上边权最大的边使得图仍然合法,能删就删掉。正确性基于随着可供选择的边越来越多,边权最大值一定会越来越小。

检查删掉某条边之后是否仍然合法,可以通过lct维护子树中的节点个数的奇偶性实现。

Code

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <set>
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x) {
	x=0; char c=getchar(); int f=1;
	while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); }
	while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
// val[x] = (x is a real node) + (real nodes from x' light sons)
const int N=2e5+10;
struct Val {
	int id,v;
	Val(int id=0,int v=0): id(id),v(v) {}
	friend bool operator <(Val A,Val B) { return A.v==B.v?A.id<B.id:A.v<B.v; }
}mx[N],val[N];

int ch[N][2],fa[N],n;
int vir[N],sum[N],rev[N];
int stk[N],top,ncnt;
inline int new_node() { return top?stk[top--]:++ncnt; }
inline void rec_node(int u) { if(top+1<N) stk[++top]=u; ch[u][0]=ch[u][1]=fa[u]=sum[u]=mx[u].v=rev[u]=0; }

inline bool isroot(int x) { return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x; }
inline bool get(int x) { return ch[fa[x]][1]==x; }
inline void Swap(int x) { swap(ch[x][0],ch[x][1]),rev[x]^=1; }
inline void push_down(int x) { if(rev[x]) Swap(ch[x][0]),Swap(ch[x][1]),rev[x]=0; }
inline void push_up(int x) { sum[x]=sum[ch[x][0]]^sum[ch[x][1]]^vir[x],mx[x]=max(max(mx[ch[x][0]],mx[ch[x][1]]),val[x]); }
inline void PD(int x) { static int stk[N],top; stk[top=1]=x; while(!isroot(x)) stk[++top]=x=fa[x]; while(top) push_down(stk[top--]); }
inline void rotate(int x) {
	int f=fa[x],ff=fa[f],d=get(x);
	fa[x]=ff; if(!isroot(f)) ch[ff][ch[ff][1]==f]=x;
	fa[ch[x][d^1]]=f; ch[f][d]=ch[x][d^1];
	fa[f]=x,ch[x][d^1]=f; push_up(f),push_up(x);
}
inline void splay(int x) { PD(x); for(int f=fa[x];!isroot(x);rotate(x),f=fa[x]) if(!isroot(f)) rotate(get(x)==get(f)?f:x); }
inline void access(int x) {
	for(int y=0;x;y=x,x=fa[x]) {
		splay(x);
		vir[x]^=sum[y],vir[x]^=sum[ch[x][1]];
		ch[x][1]=y,push_up(x);
	}
}
inline void makeroot(int x) { access(x),splay(x),Swap(x); }
inline void link(int x,int y) {
	makeroot(x),makeroot(y);
	vir[x]^=sum[y],fa[y]=x;
}
inline void cut(int x,int y) {
	makeroot(x),access(y),splay(y);
	fa[x]=ch[y][0]=0; push_up(y);
}

struct ed {
	int x,y,w,id;
	ed(int x=0,int y=0,int w=0): x(x),y(y),w(w) {};
	friend bool operator <(ed A,ed B) {
		if(A.w!=B.w) return A.w<B.w;
		if(A.x!=B.x) return A.x<B.x;
		if(A.y!=B.y) return A.y<B.y;
		return A.id<B.id;
	}
}E[300010];
int pid[300010];
set<ed> S;
int odd_cnt;
void Jud(int x,int y) {
	access(x),splay(x),access(y),splay(y);
	if(sum[x]==1&&sum[y]==1) odd_cnt-=2;
}
void Link(int id) {
	int x=E[id].x,y=E[id].y,w=E[id].w;
	int u=new_node(); pid[id]=u;
	vir[u]=0,val[u]=Val(id,w),push_up(u);
	link(x,u),link(y,u);
	S.insert(E[id]);
}
void Cut(int id) {
	int u=pid[id];
	cut(E[id].x,u),cut(E[id].y,u);
	S.erase(E[id]);
	rec_node(u);
}
void ins(int id) { // insert the edge with index x
	int x=E[id].x,y=E[id].y,w=E[id].w;
	makeroot(x),access(y),splay(y);
	if(!fa[x]) Jud(x,y),Link(id);
	else if(mx[y].v>w) Cut(mx[y].id),Link(id);
}
void getans() {
	while(!S.empty()) {
		int id=S.rbegin()->id;
		makeroot(E[id].x),access(pid[id]),splay(pid[id]);
		if(sum[pid[id]]^sum[E[id].x]) return;
		Cut(id);
	}
}
int main() {
	int n,m; rd(n),rd(m);
	ncnt=n,odd_cnt=n;
	for(int i=1;i<=n;++i) vir[i]=1,push_up(i);
	for(int i=1;i<=m;++i) {
		rd(E[i].x),rd(E[i].y),rd(E[i].w),E[i].id=i;
		ins(i);
		if(!odd_cnt) {
			getans();
			printf("%d\n",S.rbegin()->w);
		}
		else printf("-1\n");
//		cout<<"odd_cnt="<<odd_cnt<<endl;
	}
	return 0;
}

B - AGC036E ABC String

Sol

问题等价于从 S S S中删除尽可能少的字符使其满足条件。首先相邻且相同的字符是没有用的,我们只保留其中的一个。

假设出现次数从小到大分别是A,B,C

C的出现次数大于B

  1. 如果存在C,它的左右两边的字符不相同或者在序列的两端,直接把它删掉。
  2. 否则,删掉一个形如ACA的子串中的CA(这是唯一能够改变CB的出现次数差的方法)。

显然这样删完过后三种字符的出现次数大小关系不会变化。

接下来,若B的出现次数大于A,就不断地找出满足两边的字符不同的子串BC或者CB删掉。由于A的出现次数最小,所以一定能够找到BC或者CB子串。

删除用双向链表实现,总时间复杂度 O ( n ) O(n) O(n)

Code

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x) {
	x=0; char c=getchar(); int f=1;
	while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); }
	while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
const int N=1e6+10;
int S[N],mp[3],buc[3];
int n;
void change(int a,int b) {
	swap(mp[a],mp[b]);
	for(int i=1;i<=n;++i) {
		if(S[i]==a) S[i]=b;
		else if(S[i]==b) S[i]=a;
	}
}
int L[N],R[N];
void del(int x) {
	buc[S[x]]--;
	R[L[x]]=R[x];
	L[R[x]]=L[x];
}


int main() {
	mp[1]=1,mp[2]=2;
	
	{
		static char str[N];
		scanf("%s",str+1);
		int m=strlen(str+1);
		for(int i=1;i<=m;++i)
			if(i==1||str[i]!=str[i-1]) S[++n]=str[i]-'A';
	}
	
	S[0]=-1,S[n+1]=-2;
	for(int i=1;i<=n;++i) L[i]=i-1,R[i]=i+1; R[0]=1,L[n+1]=n;
	
	buc[0]=buc[1]=buc[2]=0;
	for(int i=1;i<=n;++i) buc[S[i]]++;
	
	{
		int ch=0;
		if(buc[1]<=min(buc[2],buc[0])) ch=1;
		if(buc[2]<=min(buc[1],buc[0])) ch=2;
		if(ch) change(0,ch),swap(buc[0],buc[ch]);
		if(buc[1]<buc[2]) change(1,2),swap(buc[1],buc[2]);
	}
	
	{
		int t=buc[1]-buc[2];
		for(int i=R[0];i<=n&&t;i=R[i])
			if(S[i]==1&&S[L[i]]!=S[R[i]]) del(i),t--;

		for(int i=R[0];i<=n&&t;i=R[i]) 
			if(S[i]==1&&S[L[i]]==0&&S[R[i]]==0) del(R[i]),del(i),t--;

	}
	{
		int t=buc[2]-buc[0];
		for(int i=R[0];R[i]<=n&&t;i=R[i])
			if(S[i]!=0&&S[R[i]]!=0&&S[L[i]]!=S[R[R[i]]])
				del(R[i]),del(i),t--;
	}
	for(int i=R[0];i<=n;i=R[i]) putchar('A'+mp[S[i]]);
	return 0;
}

C - AGC021E Ball Eat Chameleons

Sol

一只变色龙最终是红色的,要么它吃掉的红球比蓝球多,要么它吃掉的红球和蓝球一样多,并且它最后一个吃掉的球是蓝色的。

R R R为红球的数量, B B B为蓝球的数量。若 R < B R< B R<B显然无解;若 R = B R=B R=B,则序列的最后一个球一定是蓝色的,所以其方案数等于 R R R个红球、 B − 1 B-1 B1个蓝球的方案数;下面讨论 R > B R> B R>B的情况。

R − B ≥ n R-B\ge n RBn,我们可以让所有的变色龙吃的红球比蓝球多,所以方案数就是 ( R + B R ) {R+B\choose R} (RR+B)

R − B < n R-B< n RB<n,也就意味着我们要让 n − ( R − B ) n-(R-B) n(RB)只变色龙吃的红球和蓝球一样多,让 R − B R-B RB只变色龙吃的红球恰好比吃的蓝球多一个。可以让所有吃的红球和蓝球个数一样的变色龙都只吃一个红球和一个蓝球,多出来的那些红球和蓝球数量相同,让某一个吃的红球比蓝球多的变色龙吃掉就可以了。所以一个排列合法的条件是,能够找出 n − ( R − B ) n-(R-B) n(RB)对匹配,每个匹配是一个下标较小的红球匹配一个下标较大的蓝球。一个排列合法的充分必要条件是:对于任意一个前缀,这个前缀中的蓝球至多比红球多 B − ( n − ( R − B ) ) = R − n B-(n-(R-B)) = R-n B(n(RB))=Rn个(即至多有 R − n R-n Rn个蓝球不被用于匹配)。

考虑卡特兰数的折线模型,假设我们从 ( 0 , 0 ) (0,0) (0,0)出发,每一步可以从 ( x , y ) (x,y) (x,y)走到 ( x + 1 , y ) (x+1,y) (x+1,y)(代表红球)或者 ( x , y + 1 ) (x,y+1) (x,y+1)(代表蓝球),最终要走到 ( R , B ) (R,B) (R,B)。则蓝球总个数-红球总个数小于等于 R − n R-n Rn等价于我们的位置处于直线 y = x + R − n y=x+R-n y=x+Rn的下方。

对不合法的折线计数:从该折线经过的第一个 y > x + R − n y >x+R-n y>x+Rn的点 ( t , t + R − n + 1 ) (t,t+R-n+1) (t,t+Rn+1)将折线断开,将折线的后半段对称翻转;翻转前,后半段位置的变化是向量 ( R − t , B − t − R + n − 1 ) (R-t,B-t-R+n-1) (Rt,BtR+n1),翻转后,后半段位置的变化是向量 ( B − t − R + n − 1 , R − t ) (B-t-R+n-1,R-t) (BtR+n1,Rt),所以翻转后的折线的终点是 ( B − R + n − 1 , 2 R − n + 1 ) (B-R+n-1,2R-n+1) (BR+n1,2Rn+1)。并且,任何一条走到 ( B − R + n − 1 , 2 R − n + 1 ) (B-R+n-1,2R-n+1) (BR+n1,2Rn+1)的直线显然会穿过 y = x + R − n y=x+R-n y=x+Rn,将它沿着它第一次穿过 y = x + R − n y=x+R-n y=x+Rn时走到的位置对称翻转就可以得到一条穿过了 y = x + R − n y=x+R-n y=x+Rn的走到 ( R , B ) (R,B) (R,B)的直线。这样我们就在所有不合法的折线与任意的走到 ( B − R + n − 1 , 2 R − n + 1 ) (B-R+n-1,2R-n+1) (BR+n1,2Rn+1)的折线之间建立了双射。

所以从原点走到 ( R , B ) (R,B) (R,B)且不穿过 y = x + R − n y=x+R-n y=x+Rn的方案数为 ( R + B R ) − ( R + B 2 R − n + 1 ) {R+B\choose R} - {R+B\choose 2R-n+1} (RR+B)(2Rn+1R+B)

Code

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x) {
	x=0; char c=getchar(); int f=1;
	while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); }
	while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
const int N=5e5+10,mod=998244353;
int Pow(int x,int y) {                                                                                                                                                   
	int res=1;
	while(y) {
		if(y&1) res=res*(ll)x%mod;
		x=x*(ll)x%mod,y>>=1;
	}
	return res;
}
int fac[N],inv[N];
void getfac(int n) {
	fac[0]=1; for(int i=1;i<=n;++i) fac[i]=fac[i-1]*(ll)i%mod;
	inv[n]=Pow(fac[n],mod-2); for(int i=n;i>=1;--i) inv[i-1]=inv[i]*(ll)i%mod;
}
int C(int n,int m) { if(m>n) return 0;return fac[n]*(ll)inv[m]%mod*inv[n-m]%mod; }
int n,m;
int main() {
	rd(n),rd(m); getfac(m);
	if(m<n) {
		printf("0");
		return 0;
	}
	int ans=0;
	for(int R=0;R<=m;++R) {
		int B=m-R; if(R==B) B--;
		int L=R-B;
		if(L<0||(n-L)>B) continue;
		int T=B-(n-L);
		ans=(ans+C(R+B,R))%mod;
		if(L<n) ans=(ans-C(R+B,R+T+1))%mod;
	}	
	printf("%d",(ans+mod)%mod);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值