2020牛客暑期多校训练营(第八场)题解

A. All-Star Game

n n n 个球星和 m m m 个球迷,给出他们之间的初始关系,如果 a a a b b b 的球迷那么 a a a 喜欢看 b b b 的比赛,如果 a , c a,c a,c 都喜欢看 b b b 的比赛,并且 c c c 喜欢看 d d d 的比赛,那么 a a a 也喜欢看 d d d 的比赛。 q q q 组询问,每组询问会修改一对球迷和球星间的关系,然后询问举办一场全明星赛,至少要多少个明星才能使所有粉丝都喜欢看。

注意要分清楚a是b的粉丝a喜欢看b的比赛这两个关系,捋清楚之后,就可以发现,对于一个球星和球迷的连通块,其实只需要选其中一个球星,就可以使其中所有球迷来看。

那么就是维护总联通块数 A A A,球星孤点数 B B B,球迷孤点数 C C C 就可以了,答案为 A − B A-B AB,球星是孤点意味着选他和不选他不影响任何球迷,而如果球迷是孤点那么选哪个球星他都不来看,即如果 C > 0 C>0 C>0,此时答案为 − 1 -1 1

连通块用 L C T LCT LCT 维护一下即可,但是要注意这是图而不是树,所以要预处理出每条边被删除的时间,当加入一条新边 ( x , y ) (x,y) (x,y) 时,假如 x , y x,y x,y 原本就连通,那么看一下他们之间最早被删除的边,假如新加的边删除时间比他晚,那么就把他替换掉。

细节就看代码吧,数据结构题总不能说完所有细节吧qwq:

#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 400010
#define pb push_back
#define inf 999999999

int n,m,q;
struct edge{
    
    
	int x,y,Time;edge(int xx=0,int yy=0,int TIME=inf):x(xx),y(yy),Time(TIME){
    
    }
	bool operator <(const edge &B)const{
    
    return x==B.x?y<B.y:x<B.x;}
};
edge get(int x,int y,int Time=inf){
    
    if(x>y)swap(x,y);return (edge){
    
    x,y,Time};}
vector<edge> e;int St=0;
struct node{
    
    
	edge x;node *zuo,*you,*fa,*mi;int lazy;
	node(edge X):x(X),zuo(NULL),you(NULL),fa(NULL),mi(this),lazy(0){
    
    }
	void update(int c){
    
    lazy^=c;if(c)swap(zuo,you);}
	void pushdown(){
    
    
		if(zuo)zuo->update(lazy);
		if(you)you->update(lazy);
		lazy=0;
	}
	bool notroot(){
    
    return fa&&(fa->zuo==this||fa->you==this);}
	void check(){
    
    
		mi=this;
		if(zuo&&zuo->mi->x.Time<mi->x.Time)mi=zuo->mi;
		if(you&&you->mi->x.Time<mi->x.Time)mi=you->mi;
	}
}*d[maxn];
map<edge,node*>mp;
map<edge,int>sta;//status
void rotate(node *x){
    
    
	node *fa=x->fa,*gfa=fa->fa;
	if(fa->zuo==x){
    
    
		fa->zuo=x->you;
		if(x->you)x->you->fa=fa;
		x->you=fa;
	}else{
    
    
		fa->you=x->zuo;
		if(x->zuo)x->zuo->fa=fa;
		x->zuo=fa;
	}
	fa->fa=x,x->fa=gfa;
	if(gfa&&gfa->zuo==fa)gfa->zuo=x;
	if(gfa&&gfa->you==fa)gfa->you=x;
	fa->check();x->check();
}
node *zhan[maxn];int t=0;
#define witch(x) (x->fa->zuo==x)
void splay(node *x){
    
    
	node *p=x;zhan[++t]=x;
	while(p->notroot())zhan[++t]=p=p->fa;
	while(t)zhan[t--]->pushdown();
	while(x->notroot()){
    
    
		if(x->fa->notroot()&&witch(x)==witch(x->fa))rotate(x->fa),rotate(x);
		else rotate(x);
	}
}
void access(node *x){
    
    for(node *y=NULL;x;y=x,x=x->fa)splay(x),x->you=y,x->check();}
void makeroot(node *x){
    
    access(x);splay(x);x->update(1);}
node *findrt(node *x){
    
    access(x);splay(x);while(x->zuo)x=x->zuo;return x;}
void link(node *x,node *y){
    
    makeroot(x);x->fa=y;}
void del(node *x,node *y){
    
    makeroot(x);access(y);splay(x);x->you=y->fa=NULL;x->check();}
//以上为LCT板子
int du[maxn],cnt,gu1,gu2;//du记录每个点的度,cnt记录连通块数,gu1,gu2记录球星球迷孤点数
void addedge(int x,int y,int Time){
    
    
	cnt--;
	if(!du[x]++)if(x<=n)gu1--;else gu2--;
	if(!du[y]++)if(y<=n)gu1--;else gu2--;
	edge p=get(x,y,Time);
	mp[p]=new node(p);
	link(d[x],mp[p]);link(d[y],mp[p]);
}
void deledge(int x,int y,int Time){
    
    
	cnt++;
	if(!--du[x])if(x<=n)gu1++;else gu2++;
	if(!--du[y])if(y<=n)gu1++;else gu2++;
	edge p=get(x,y,Time);
	del(d[x],mp[p]);del(d[y],mp[p]);
}
void Link(int x,int y,int Time){
    
    
	node *X=d[x],*Y=d[y];
	if(findrt(X)==findrt(Y)){
    
    
		makeroot(X);access(Y);splay(X);
		edge &p=X->mi->x;
		if(p.Time<Time){
    
    
			sta[p]=2;sta[get(x,y,Time)]=1;
			deledge(p.x,p.y,p.Time),addedge(x,y,Time);
		}else sta[get(x,y,Time)]=2;
	}else addedge(x,y,Time),sta[get(x,y,Time)]=1;
}

int main()
{
    
    
	scanf("%d %d %d",&n,&m,&q);cnt=(gu1=n)+(gu2=m);
	for(int i=1;i<=n+m;i++)d[i]=new node((edge){
    
    0,0,inf});
	for(int i=1;i<=n;i++){
    
    
		int k,x;scanf("%d",&k);St+=k;
		while(k--)scanf("%d",&x),x+=n,e.pb(get(i,x)),sta[e.back()]=e.size()-1;
	}
	for(int i=1,x,y;i<=q;i++){
    
    
		scanf("%d %d",&x,&y);x+=n;
		edge p=get(x,y);e.pb(p);
		if(!sta.count(p)||sta[p]==-1)sta[p]=e.size()-1;
		else e[sta[p]].Time=e.back().Time=i,sta[p]=-1;
	}
	sta.clear();
	for(int i=0;i<St;i++)Link(e[i].x,e[i].y,e[i].Time);
	for(int i=St;i<St+q;i++){
    
    
		if(!sta.count(e[i])||sta[e[i]]==0){
    
    
			sta[e[i]]=1,Link(e[i].x,e[i].y,e[i].Time);
		}else{
    
    
			if(sta[e[i]]==1)deledge(e[i].x,e[i].y,e[i].Time);
			sta[e[i]]=0;
		}
		if(gu2)printf("-1\n");
		else printf("%d\n",cnt-gu1);
	}
}

C. Cinema

有一个 n × m n\times m n×m 的网格图,你要在上面放最少的人,使得任意两人不相邻,且不能放更多的人,给出一个方案。

m m m 只有 15 15 15 容易想到状压,理所当然地用 0 0 0 表示没人 1 1 1 表示有人然后行与行之间转移。

然后发现没有办法判断是否还剩下可以放的位置……这种 dp \text{dp} dp 只能做最多人做不了最少人。

于是需要加一维状态,容易发现最后的图中,对于任意位置,不是有人就是旁边有人,为了方便转移,令 0 0 0 表示这个位置下面有人, 1 1 1 表示这个位置的上、左、右至少有一个人, 2 2 2 表示这个位置有人。

d f s dfs dfs 找到所有合法状态,不合法状态满足:存在 22 22 22 00 00 00 11 11 11 的两边没有 2 2 2(这意味着上面要放两个连续的 2 2 2)。

对于每个状态,再找到它下一行的合法状态,下一行的状态要在上面的限制的基础上再满足一些限制: 2 2 2 下面必须是 1 1 1 0 0 0 下面必须是 2 2 2 1 1 1 的上、左、右至少有一个 2 2 2

然后大力 dp \text{dp} dp 就可以了,题解搜出来的状态是六七千个,但是我搜出来的状态有一万四,可能是有什么定义不够优秀吧……当时本地跑需要 7 s 7s 7s 左右,临走前自信交了一发居然过了,挺突然的……

代码如下:

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define pb push_back

struct par{
    
    
	int x,pos;
	bool operator <(const par &B)const{
    
    return x<B.x;}
};
vector<par> q[20];
vector<int> sta;
int id[13000010],idtot;
void print(int x,int m);
void dfs(int x,int now,int m){
    
    
	if(x==m){
    
    
		id[now]=idtot++;
		sta.pb(now);
		return;
	}
	if(x==0)for(int i=2;i>=0;i--)dfs(x+1,i,m);
	else if(now%3!=1)dfs(x+1,now*3+1,m);//0,2 -> 1
	else{
    
    
		dfs(x+1,now*3+2,m);//1 -> 2
		if(x==2&&now%3==1&&now/3%3==1)return;
		if(x>2&&now%3==1&&now/3%3==1&&now/9%3==1)return;
		dfs(x+1,now*3+0,m);//1 -> 0,1
		dfs(x+1,now*3+1,m);
	}
}
int thi[20];
vector<int> ne[14010];
void dfs2(vector<int> &vec,int st,int x,int now,int m){
    
    
	if(x==m){
    
    
		for(int i=0;i<m;i++)
		if(now/thi[i]%3==1&&st/thi[i]%3!=2&&(i==0||now/thi[i-1]%3!=2)&&(i==m-1||now/thi[i+1]%3!=2))return;
		vec.pb(id[now]);return;
	}
	int p=st/thi[m-x-1]%3;
	if(x==0){
    
    
		if(p==1)dfs2(vec,st,x+1,now*3+0,m);
		if(p!=0)dfs2(vec,st,x+1,now*3+1,m);
		if(p!=2)dfs2(vec,st,x+1,now*3+2,m);
	}else if(now%3!=1){
    
    
		if(p!=0)dfs2(vec,st,x+1,now*3+1,m);
	}
	else{
    
    
		if(p!=2)dfs2(vec,st,x+1,now*3+2,m);
		if(x==2&&now%3==1&&now/3%3==1)return;
		if(x>2&&now%3==1&&now/3%3==1&&now/9%3==1)return;
		if(p==1)dfs2(vec,st,x+1,now*3+0,m);
		if(p!=0)dfs2(vec,st,x+1,now*3+1,m);
	}
}
int f[1010][14010],fa[1010][14010],two[14010];
int count0(int x,int m){
    
    
	int re=0;
	for(int i=0;i<m;i++)if(x/thi[i]%3==0)re++;
	return re;
}
int count2(int x,int m){
    
    
	int re=0;
	for(int i=0;i<m;i++)if(x/thi[i]%3==2)re++;
	return re;
}
bool SetFirst(int x,int m){
    
    
	for(int i=0;i<m;i++)
	if(x/thi[i]%3==1&&(i==0||x/thi[i-1]%3!=2)&&(i==m-1||x/thi[i+1]%3!=2))return false;
	return true;
}
vector<int> ed,ans[1010];
void print(int p,int m){
    
    
	for(int i=0;i<m;i++){
    
    
		if(p/thi[i]%3==0)printf("0");
		if(p/thi[i]%3==1)printf("1");
		if(p/thi[i]%3==2)printf("2");
	}
}
void work(int m){
    
    
	sta.clear();idtot=0;dfs(0,0,m);
	for(int i=0;i<idtot;i++){
    
    
		ne[i].clear(),dfs2(ne[i],sta[i],0,0,m);
	}
	const int N=1000;
	for(int i=0;i<=N;i++)for(int j=0;j<idtot;j++)f[i][j]=1e9,fa[i][j]=-1;
	ed.clear();
	for(int j=0;j<idtot;j++){
    
    
		two[j]=count2(sta[j],m);
		if(SetFirst(sta[j],m))f[1][j]=two[j];
		if(!count0(sta[j],m))ed.pb(j);
	}
	sort(q[m].begin(),q[m].end());int st=0;
	for(int i=1;i<=N;i++){
    
    
		while(st<q[m].size()&&q[m][st].x==i){
    
    
			int pos=q[m][st].pos,now=i,k=ed[0];
			for(int j:ed)if(f[i][j]<f[i][k])k=j;ans[pos].pb(f[i][k]);
			while(now>0)ans[pos].pb(sta[k]),k=fa[now--][k];
			st++;
		}
		if(i<N)for(int j=0;j<idtot;j++){
    
    
			for(int k:ne[j]){
    
    
				if(f[i+1][k]>f[i][j]+two[k]){
    
    
					f[i+1][k]=f[i][j]+two[k];
					fa[i+1][k]=j;
				}
			}
		}
	}
}
int T;
struct que{
    
    int n,m;}Q[1010];

int main()
{
    
    
	scanf("%d",&T);for(int _=1;_<=T;_++){
    
    
		scanf("%d %d",&Q[_].n,&Q[_].m);
		q[Q[_].m].pb((par){
    
    Q[_].n,_});
	}
	thi[0]=1;for(int i=1;i<15;i++)thi[i]=3*thi[i-1];
	for(int i=1;i<=15;i++)work(i);
	for(int _=1;_<=T;_++){
    
    
		int m=Q[_].m;
		printf("Case #%d: %d\n",_,ans[_][0]);
		while(Q[_].n--){
    
    
			int p=ans[_].back();ans[_].pop_back();
			for(int i=0;i<m;i++){
    
    
				if(p/thi[i]%3==0)printf(".");//这里写的这么烦是因为调试的时候搞得,后来懒得改了……
				if(p/thi[i]%3==1)printf(".");
				if(p/thi[i]%3==2)printf("*");
			}
			printf("\n");
		}
	}
}

E. Enigmatic Partition

n n n 的一个优秀的整数拆分满足: a i ≤ a i + 1 ≤ a i + 1 , a m = a 1 + 2 a_i\leq a_{i+1}\leq a_i+1,a_m=a_1+2 aiai+1ai+1,am=a1+2,令 f ( k ) f(k) f(k) 表示 k k k 的优秀整数拆分数量,求 ∑ i = l r f ( i ) \sum_{i=l}^r f(i) i=lrf(i)

一个优秀的整数分拆肯定由 d , d + 1 , d + 2 d,d+1,d+2 d,d+1,d+2 三个数组成,相当于先往 m m m 个位置上全部放 d d d,然后使一个后缀 + 1 +1 +1,再使另一个后缀 + 1 +1 +1,这两个后缀不能相同。

考虑一个暴力:令 N = 1 0 5 N=10^5 N=105,枚举 a 1 a_1 a1,枚举序列长度 m m m,再枚举 k k k 表示两次后缀加一一共加了多少,可以发现 k k k 的枚举范围只能到 2 m 2m 2m m m m 的枚举范围只到 ⌊ N a 1 ⌋ \lfloor \frac N {a_1} \rfloor a1N,所以时间复杂度为:
∑ i = 1 N ⌊ N i ⌋ 2 \sum_{i=1}^N \lfloor \frac N i \rfloor^2 i=1NiN2

i i i 比较小时时间复杂度会很高,但此时我们可以用完全背包来做,于是时间复杂度就不高了。

代码如下:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 100010
#define ll long long

int T,l,r;
ll f[maxn],sum[maxn];
void work(){
    
    
	int N=100000,block=100;
	for(int j=1;j<block;j++){
    
    
		memset(f,0,sizeof(f));
		f[j+j+1+j+2]=1;
		for(int k=0;k<=2;k++){
    
    
			for(int i=3*j+3;i<=N;i++)
			if(i+j+k<=N)f[i+j+k]+=f[i];
		}
		for(int i=3*j+3;i<=N;i++)sum[i]+=f[i];
	}
	for(int i=block;i<=N;i++){
    
    //a[1] 
		for(int j=3;i*j<=N;j++){
    
    //len 
			for(int k=3;k<2*(j-1)&&i*j+k<=N;k++){
    
    
				if(k>j-1)sum[i*j+k]+=(2*(j-1)-k+1)/2;
				else sum[i*j+k]+=(k-1)/2;
			}
		}
	}
	for(int i=1;i<=N;i++)sum[i]+=sum[i-1];
}

int main()
{
    
    
	work();scanf("%d",&T);for(int _=1;_<=T;_++)
	scanf("%d %d",&l,&r),printf("Case #%d: %lld\n",_,sum[r]-sum[l-1]);
}

还有另一个更优秀的做法,时间复杂度严格 O ( n ln ⁡ n ) O(n\ln n) O(nlnn) 而且常数不超过 2 2 2,在这里

大致的思想是:先对 f f f 做差分,枚举 a 1 a_1 a1 m m m,发现从 a 1 × m + 3 a_1\times m+3 a1×m+3 开始每隔一位 f f f + 1 +1 +1,然后从 ( a 1 + 1 ) × m + 1 (a_1+1)\times m+1 (a1+1)×m+1 开始又会连续减一,但是又发现从 ( a 1 + 2 ) × m − 3 (a_1+2)\times m-3 (a1+2)×m3 (包括这一位)往前每隔一位就会同时出现 + 1 +1 +1 − 1 -1 1 可以抵消掉,那么剩下的 + 1 , − 1 +1,-1 +1,1 就都是隔一位出现的了,再做一次二阶差分,那么每次修改就可以 O ( 1 ) O(1) O(1) 实现。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 300010

int T,l,r;
long long f[maxn];

int main()
{
    
    
	for(int i=1;i<=100000;i++){
    
    
		for(int j=1;i*j<=100000;j++){
    
    
			f[i*j+3]++;
			f[i*j+3+j-2]--;
			f[i*j+3+j-2+1]--;
			f[(i+2)*j-3+3]++;
		}
	}
	for(int i=2;i<=100000;i++)f[i]+=f[i-2];
	for(int i=1;i<=100000;i++)f[i]+=f[i-1];
	for(int i=1;i<=100000;i++)f[i]+=f[i-1];
	scanf("%d",&T);for(int _=1;_<=T;_++)
	scanf("%d %d",&l,&r),printf("Case #%d: %lld\n",_,f[r]-f[l-1]);
}

G. Game SET

n n n 张牌,每张牌有四个属性,三张牌能够组成SET当且仅当每个属性都满足:三张牌的该属性都相同或都不同。找出一个SET。

枚举前两张,用 map \text{map} map 判对应的第三张是否存在即可,时间复杂度 O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn)

代码如下:

#include <cstdio>
#include <string>
#include <map>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define pb push_back

int T,n;
struct card{
    
    
	int val[4];
	bool operator <(const card &B)const{
    
    
		return val[0]==B.val[0]?(val[1]==B.val[1]?(val[2]==B.val[2]?val[3]<B.val[3]:val[2]<B.val[2]):val[1]<B.val[1]):val[0]<B.val[0];
	}
}a[310];
char s[110];
string p;
string find(int st){
    
    
	string re;re.clear();
	for(int i=st+1;s[i]!=']';i++)re+=s[i];
	return re;
}
map<card,int> mp;
vector<int> C[4];

int main()
{
    
    
	scanf("%d",&T);for(int Test=1;Test<=T;Test++)
	{
    
    
		scanf("%d",&n);mp.clear();
		for(int i=1;i<=n;i++){
    
    
			scanf("%s",s+1);
			int st=1;p=find(st);
			if(p=="*")a[i].val[0]=0;
			if(p=="one")a[i].val[0]=1;
			if(p=="two")a[i].val[0]=2;
			if(p=="three")a[i].val[0]=3;
			st+=p.length()+2;p=find(st);
			if(p=="*")a[i].val[1]=0;
			if(p=="diamond")a[i].val[1]=1;
			if(p=="squiggle")a[i].val[1]=2;
			if(p=="oval")a[i].val[1]=3;
			st+=p.length()+2;p=find(st);
			if(p=="*")a[i].val[2]=0;
			if(p=="open")a[i].val[2]=1;
			if(p=="solid")a[i].val[2]=2;
			if(p=="striped")a[i].val[2]=3;
			st+=p.length()+2;p=find(st);
			if(p=="*")a[i].val[3]=0;
			if(p=="red")a[i].val[3]=1;
			if(p=="green")a[i].val[3]=2;
			if(p=="purple")a[i].val[3]=3;
			mp[a[i]]=i;
		}
		printf("Case #%d: ",Test);
		bool ans=false;
		for(int i=1;i<=n;i++){
    
    
			for(int j=i+1;j<=n;j++){
    
    
				for(int k=0;k<4;k++){
    
    
					C[k].clear();C[k].pb(0);
					if(a[i].val[k]==0||a[j].val[k]==0)for(int p=1;p<4;p++)C[k].pb(p);
					else if(a[i].val[k]==a[j].val[k])C[k].pb(a[i].val[k]);
					else C[k].pb(6-a[i].val[k]-a[j].val[k]);
				}
				for(int c0:C[0])for(int c1:C[1])for(int c2:C[2])for(int c3:C[3]){
    
    
					card now=(card){
    
    c0,c1,c2,c3};
					if(!ans&&mp.count(now)&&i!=mp[now]&&j!=mp[now]){
    
    
						ans=true;
						printf("%d %d %d\n",i,j,mp[now]);
					}
				}
				if(ans)break;
			}
			if(ans)break;
		}
		if(!ans)printf("-1\n");
	}
}

题解用到一个结论: 21 21 21 张牌内必定存在SET,所以可以 O ( 2 1 3 ) O(21^3) O(213) 直接暴力……

H. hard String Problem

给出 n n n 个字符串,将他们以自己为循环节无限循环,然后问这些字符串有多少个相同的子串。

先将所有字符串变成自己的最小循环节,然后就有一个结论:两个无限循环字符串的公共子串长度不超过长串长度的三倍,证明参照题解:
在这里插入图片描述
于是可以考虑让所有字符串去和最短的那个匹配,那么就只需要将长度变成原来的四倍就够了,而不需要无限长,而最短的子串长度要翻倍到最长子串的四倍长度。

还要判断无限解:假如所有字符串本质相同,那么就有无限个解。

题解代码中有很多细节值得学习:

  1. 求自身的最短循环节可以用kmp,假设字符串由 p p p 个最短循环节组成,那么 ( p − 1 ) (p-1) (p1) 个循环节组成的字符串一定是原串的一个border(相同前后缀),用kmp可以找;
  2. 判断两个字符串是否本质相同:如果是,那么其中一个旋转若干次之后一定能得到另一个,那么对于所有字符串,将其旋转成字典序最小的形式,然后直接去重;
  3. 求解时需要对最短的子串建SAM,然后找有多少个子串在所有字符串中都有出现。实现的时候将其他字符串丢到这个SAM上跑,将出现过的子串全部标记,最后看哪些子串恰好每次都被标记过即可,这样暴力标记复杂度其实是 O ( N 2 N ) O(N\sqrt {2N}) O(N2N ) 的,证明参考这里

代码如下:

#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 2000010
#define ll long long
#define pb push_back

int n;
char cs[maxn];
string S[maxn];
namespace KMP{
    
    
	int len;
	char st[maxn];
	int next[maxn];
	void init(const char *s){
    
    
		len=strlen(s+1);
		memcpy(st,s,(len+2)*sizeof(char));
		next[0]=-1;next[1]=0;
		for(int i=1;i<len;i++){
    
    
			int j=next[i];
			while(j!=-1&&st[j+1]!=st[i+1])j=next[j];
			next[i+1]=j+1;
		}
	}
	int solve(){
    
    
		int j=next[len];
		while(j!=-1){
    
    
			if(len%(len-j)==0)return len-j;
			j=next[j];
		}
	}
}
int min_pos(char *s){
    
    //这个是找:将s旋转成字典序最小的形式,需要将前多少位放到后面
	int len=strlen(s);
	int i=0,j=1,k=0;
	while(i<len&&j<len&&k<len){
    
    
	//这部分可以这样理解:用i记录当前字典序最小的开头位置,j往后尝试找字典序更小的位置,找到了的话i就跳到j那里
		int p=s[(i+k)%len]-s[(j+k)%len];
		if(p==0)k++;
		else{
    
    
			if(p>0)i+=k+1;
			else j+=k+1;
			if(i==j)j++;
			k=0;
		}
	}
	return i;
}
string handle(char *s){
    
    
	KMP::init(s);
	int len=KMP::solve();
	s[len+1]=0;int pos=min_pos(s+1);
	for(int i=1;i<=pos;i++)s[i+len]=s[i];
	s[len+pos+1]=0;return string(s+pos+1);
}
void chkmin(int &x,int y){
    
    if(y<x)x=y;}
void chkmax(int &x,int y){
    
    if(y>x)x=y;}
namespace SAM{
    
    
	struct state{
    
    int ne[26],len,link;}st[maxn<<1];
	int now,p,q,id=0,last=0;
	int v[maxn<<1],num=0;
	int mat_max[maxn<<1],match_len[maxn<<1],match_times[maxn<<1];
	void init(){
    
    
		st[0].link=-1;
		memset(v,0,sizeof(v));
		memset(match_len,63,sizeof(match_len));
	}
	void extend(int x){
    
    
		now=++id;st[now].len=st[last].len+1;
		for(p=last;p!=-1&&!st[p].ne[x];p=st[p].link)st[p].ne[x]=now;
		if(p!=-1){
    
    
			q=st[p].ne[x];
			if(st[p].len+1==st[q].len)st[now].link=q;
			else{
    
    
				int clone=++id;
				st[clone]=st[q];st[clone].len=st[p].len+1;
				for(;p!=-1&&st[p].ne[x]==q;p=st[p].link)st[p].ne[x]=clone;
				st[q].link=st[now].link=clone;
			}
		}
		last=now;
	}
	struct par{
    
    int x,len;};
	vector<par> nodes;
	vector<int> a,b;
	bool cmp(int x,int y){
    
    return st[x].len>st[y].len;}
	void solve(string &s){
    
    
		nodes.clear();a.clear();b.clear();
		int now=0,len=0,n=s.length();
		for(int i=0;i<n;i++){
    
    
			int p=s[i]-'a';
			while(now!=0&&!st[now].ne[p])now=st[now].link,len=st[now].len;
			if(st[now].ne[p]){
    
    
				now=st[now].ne[p];
				len++;
			}
			nodes.pb((par){
    
    now,len});
			a.pb(now);
		}
		sort(a.begin(),a.end(),cmp);num++;
		for(int x:a){
    
    int i=x;
			while(i!=0&&v[i]!=num){
    
    
				b.pb(i);mat_max[i]=0;
				v[i]=num;i=st[i].link;
			}
		}
		for(par i:nodes)chkmax(mat_max[i.x],i.len);
		sort(b.begin(),b.end(),cmp);
		for(int i:b){
    
    
			chkmax(mat_max[st[i].link],st[st[i].link].len);
			chkmin(match_len[i],mat_max[i]);
			match_times[i]++;
		}
	}
	ll get_ans(int tot_times){
    
    
		ll re=0;
		for(int i=1;i<=id;i++)
		if(match_times[i]==tot_times){
    
    
			re+=max(0,match_len[i]-st[st[i].link].len);
		}
		return re;
	}
}

int main()
{
    
    
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
    
    
		cin>>cs+1;
		S[i]=handle(cs);
	}
	sort(S+1,S+n+1);
	n=unique(S+1,S+n+1)-S-1;
	if(n==1){
    
    
		cout<<-1;
		return 0;
	}
	int min_len=1e9,max_len=0;
	for(int i=1;i<=n;i++){
    
    
		chkmin(min_len,S[i].length());
		chkmax(max_len,S[i].length());
	}
	SAM::init();
	for(int i=1;i<=n;i++)if(S[i].length()==min_len){
    
    
		for(int j=min_len;j<=max_len*4;j+=min_len)
		for(int k=0;k<min_len;k++)SAM::extend(S[i][k]-'a');
		min_len=i;
		break;
	}
	for(int i=1;i<=n;i++)if(i!=min_len){
    
    
		S[i]=S[i]+S[i];S[i]=S[i]+S[i];//变成原来的四倍长度
		SAM::solve(S[i]);
	}
	cout<<SAM::get_ans(n-1);
	return 0;
}

I. Interesting Computer Game

n n n 组数字,每组两个数,你可以从每组中选一个数,问你最多能选出多少个不同的数。

这题看起来酷似网络流,但是你硬跑的话大概是会T飞的。

考虑每组内的两个数连边,那么对于一个连通块,如果边数为点数减一(即是棵树),那么能拿的点只有 n − 1 n-1 n1 个,否则总是能拿完每个点,策略大概就是每次取叶子,有环的话怎么取都行。

代码如下:

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 200010
#define pb push_back

int T,n,a[maxn][2];
vector<int> b;
int fa[maxn],v[maxn],c[maxn];
int findfa(int x){
    
    return x==fa[x]?x:fa[x]=findfa(fa[x]);}

int main()
{
    
    
	scanf("%d",&T);
	for(int _=1;_<=T;_++)
	{
    
    
		scanf("%d",&n);b.clear();
		for(int i=1;i<=n;i++){
    
    
			scanf("%d %d",&a[i][0],&a[i][1]);
			b.pb(a[i][0]);b.pb(a[i][1]);
		}
		sort(b.begin(),b.end());
		b.erase(unique(b.begin(),b.end()),b.end());
		for(int i=1;i<=2*n;i++)fa[i]=i,v[i]=0;
		for(int i=1;i<=n;i++){
    
    
			int x=lower_bound(b.begin(),b.end(),a[i][0])-b.begin()+1;
			int y=lower_bound(b.begin(),b.end(),a[i][1])-b.begin()+1;
			c[x]=c[y]=1;
			x=findfa(x),y=findfa(y);
			if(x!=y)fa[y]=x,v[x]|=v[y];
			else v[x]=1;
		}
		int ans=0;
		for(int i=1;i<=2*n;i++)if(c[i]){
    
    
			ans++;
			if(fa[i]==i&&!v[i])ans--;
		}
		printf("Case #%d: %d\n",_,ans);
	}
}

J. Jumping Points

给你 n n n 条线段,第 i i i 条线段的坐标为 ( x , l i (x,l_i (x,li ~ r i ) r_i) ri),你要在每一条线段上选一个点,令 d i s ( i , i + 1 ) dis(i,i+1) dis(i,i+1) 表示第 i i i 个点到第 i + 1 i+1 i+1 个点的距离,要求最小化 ∑ i = 1 n − 1 d i s ( i , i + 1 ) \sum_{i=1}^{n-1} dis(i,i+1) i=1n1dis(i,i+1)

先考虑固定起点和终点的情况,那么此时相当于要从起点拉一条线到终点,为了让距离最小,线应该是紧绷的,就像这样:
在这里插入图片描述
可以发现,折线的拐点只可能在线段的端点上,所以可以考虑贪心。

对于当前位置,他的后面的线段会限制他的视野,找到视野外的最近的线段,为了到达让这条线段在视野内,肯定要贪心地走到限制了当前位置看不到那条视野外的线段的端点上。

比如说上面那副图,假设当前点是起点,那么第一条在视野外的线段为下面的红色线段:
在这里插入图片描述
其中,限制了当前位置看不到那条视野外的线段的端点是紫色节点,所以下一步肯定走到紫色节点上。

这样贪心可能是 n 2 n^2 n2 的,可以考虑维护出以当前点为最左端的凸包,维护一个以每条线段的上端点形成的下凸包,维护一个以每条线段下端点形成的上凸包,那么两个凸包大概就形成了一个喇叭的样子,这喇叭最左端是当前位置,往右一位的两个点分别限制了视野的下方和上方。

这是固定了起点和终点的情况,事实上我们只需要求出 最左边的线段的上下端点+最右边的线段的上下端点 四种组合作为起点终点的情况的解就好了。

还有一些细节就看代码吧,全部讲完就太长了:

#include <cstdio>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
#define maxn 100010
#define pb push_back
#define eps 1e-7

int T,n;
struct point{
    
    
	int x;double y;
	point(int xx=0,double yy=0):x(xx),y(yy){
    
    }
};
int l[maxn],r[maxn];
vector<double> work(int Sy,int Ty){
    
    
	vector<double> re;re.clear();re.pb(Sy);
	int L1=l[1],R1=r[1],Ln=l[n],Rn=r[n];
	l[1]=r[1]=Sy;l[n]=r[n]=Ty;
	static point ql[maxn],qr[maxn];//维护上下凸包的两个单调队列
	static int stl,edl,str,edr;
	ql[stl=edl=1]=point(1,Sy);
	qr[str=edr=1]=point(1,Sy);
	int nowx=1,nowy=Sy,next=2;
	while(nowx<n){
    
    
		int nex=n,ney=l[n];
		while(next<=n){
    
    
			if(stl<edl && 1ll*(r[next]-ql[stl].y)*(ql[stl+1].x-ql[stl].x)<
				1ll*(ql[stl+1].y-ql[stl].y)*(next-ql[stl].x)){
    
    //next线段位于视野下方
					nex=ql[stl+1].x;ney=ql[stl+1].y;stl++;
					qr[str=edr=1]=point(nex,ney);
					break;
				}
			if(str<edr && 1ll*(l[next]-qr[str].y)*(qr[str+1].x-qr[str].x)>
				1ll*(qr[str+1].y-qr[str].y)*(next-qr[str].x)){
    
    //next线段位于视野上方
					nex=qr[str+1].x;ney=qr[str+1].y;str++;
					ql[stl=edl=1]=point(nex,ney);
					break;
				}
			
			while(stl<edl && 1ll*(l[next]-ql[edl].y)*(ql[edl].x-ql[edl-1].x)>=
				1ll*(ql[edl].y-ql[edl-1].y)*(next-ql[edl].x))edl--;//上凸包,斜率不允许增
			ql[++edl]=point(next,l[next]);
			while(str<edr && 1ll*(r[next]-qr[edr].y)*(qr[edr].x-qr[edr-1].x)<=
				1ll*(qr[edr].y-qr[edr-1].y)*(next-qr[edr].x))edr--;//下凸包,斜率不允许减
			qr[++edr]=point(next,r[next]);
			
			next++;
		}
		for(int i=nowx+1;i<=nex;i++){
    
    //从(nowx,nowy)走到(nex,ney),路径是沿直线走
			re.pb(1.0*(ney-nowy)/(nex-nowx)*(i-nowx)+nowy);
		}
		nowx=nex,nowy=ney;
	}
	l[1]=L1,r[1]=R1,l[n]=Ln,r[n]=Rn;
	int ml=0,mr=1e9;
	for(int i=1;i<=n;i++){
    
    //处理特殊情况,开头的若干个点可能不需要取线段的端点
		ml=max(ml,l[i]);
		mr=min(mr,r[i]);
		if(re[i-1]<ml-eps||re[i-1]>mr+eps){
    
    
			for(int j=i-2;j>=0;j--)re[j]=re[i-2];
			break;
		}
	}
	ml=0,mr=1e9;
	for(int i=n;i>=1;i--){
    
    //结尾的若干个点也可能不需要取线段端点
		ml=max(ml,l[i]);
		mr=min(mr,r[i]);
		if(re[i-1]<ml-eps||re[i-1]>mr+eps){
    
    
			for(int j=i;j<n;j++)re[j]=re[i];
			break;
		}
	}
	return re;
}
double calc(vector<double> pos){
    
    
	double re=0;
	for(int i=1;i<pos.size();i++){
    
    
		re+=sqrt(1+(pos[i]-pos[i-1])*(pos[i]-pos[i-1]));
	}
	return re;
}

int main()
{
    
    
	scanf("%d",&T);for(int _=1;_<=T;_++)
	{
    
    
		scanf("%d",&n);
		int ml=0,mr=1e9;
		for(int i=1;i<=n;i++){
    
    
			scanf("%d %d",&l[i],&r[i]);
			ml=max(ml,l[i]),mr=min(mr,r[i]);
		}
		printf("Case #%d:\n",_);
		if(ml<=mr){
    
    
			for(int i=1;i<=n;i++)printf("%d %d\n",i,ml);
			continue;
		}
		vector<double> ans1=work(l[1],l[n]);
		vector<double> ans2=work(l[1],r[n]);
		vector<double> ans3=work(r[1],l[n]);
		vector<double> ans4=work(r[1],r[n]);
		vector<double> ans=ans1;double val=calc(ans),tmp;
		if((tmp=calc(ans2))<val)ans=ans2,val=tmp;
		if((tmp=calc(ans3))<val)ans=ans3,val=tmp;
		if((tmp=calc(ans4))<val)ans=ans4,val=tmp;
		for(int i=1;i<=n;i++)printf("%d %.6lf\n",i,ans[i-1]);
	}
}

K. Kabaleo Lite

有一家餐厅,第 i i i 道菜收益为 a i a_i ai,有 b i b_i bi 碟,给每个客人的菜一定是一个前缀,并且前缀内每道菜各给一道,问招待最多客人的前提下最大收益是多少。

答案会爆 long long \text{long~long} long long 确实被阴到了……刚好就爆一点……

贪心,每次取贡献最大的前缀即可。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010
#define ll long long

int T,n,a[maxn],b[maxn],mi[maxn];
struct prefix{
    
    int x;ll sum;}s[maxn];
bool cmp(prefix x,prefix y){
    
    return x.sum>y.sum;}
char op[maxn];int t=0;
void write(__int128 x){
    
    
	if(x<0)putchar('-'),x=-x;
	if(x==0)putchar('0');
	while(x)op[++t]=x%10+'0',x/=10;
	while(t)putchar(op[t--]);
}

int main()
{
    
    
	scanf("%d",&T);for(int Test=1;Test<=T;Test++)
	{
    
    
		scanf("%d",&n);
		s[0].sum=0;mi[0]=1e9;
		for(int i=1;i<=n;i++){
    
    
			scanf("%d",&a[i]);
			s[i]=(prefix){
    
    i,s[i-1].sum+a[i]};
		}
		for(int i=1;i<=n;i++)scanf("%d",&b[i]),mi[i]=min(mi[i-1],b[i]);
		sort(s+1,s+n+1,cmp);
		int guest=0;__int128 ans=0;
		for(int i=1;i<=n;i++){
    
    
			int x=s[i].x;ll sum=s[i].sum;
			if(mi[x]<=guest)continue;
			ans+=(__int128)sum*(mi[x]-guest);guest=mi[x];
		}
		printf("Case #%d: %d ",Test,guest);
		write(ans);printf("\n");
	}
}

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/108539866