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

本文深入探讨了多项算法竞赛中的核心技巧,包括可撤销并查集、线段树、状压DP、差分技巧等高级数据结构和算法,以及如何在实际问题中巧妙运用这些技巧。文章通过具体实例解析了算法的实现细节,旨在帮助读者提升解决复杂问题的能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

A All-Star Game

n n n个球员, m m m名粉丝.
我们现在已知一些喜欢关系(粉丝 → \rightarrow 球员).
然后同喜欢一个球员的两个粉丝有相同的喜好.
只有自己喜欢的球员上场自己才去看.
判定至少选多少球员才能让所有粉丝到场.

我们可以考虑可撤销并查集+线段树.
把边的修改范围在线段树上定位,每个节点维护一个 v e c t o r vector vector,表示正好覆盖这个区间的边.
之所以用到线段树的原因是加入和删除是连续的操作,不会对其他部分造成影响.(回溯得干干净净).
总空间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),时间复杂度为 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n).

如果考虑用 L C T LCT LCT求的话,可以做到空间 O ( n ) O(n) O(n),时间 O ( n log ⁡ n ) O(n\log n) O(nlogn).

可撤销并查集:

const int N=5e5+10,M=7e5+10;

int n,m,q,fa[N],sz[N],sum,ans;//sum表示有度的粉丝数量,ans表示粉丝所占的连通块数量. 
int get(int x) {return fa[x] == x? x: get(fa[x]);}
vector<int> V[N*2]; 

map<pii,int> S;//边 
struct E{int x,y; } e[M]; int tot,L[M];

struct rec {int x,y,sx,sy,sum,ans;}sta[M]; int top;
//历史修改,可回撤并查集(我们把时间看作一个维度,线段树维护边的出现时间范围).

void pop() {//不可路径压缩,我们按秩合并且暴力回撤 
	rec t=sta[top--];
	fa[t.x]=t.x; fa[t.y]=t.y;
	sz[t.x]=t.sx;sz[t.y]=t.sy;
	sum=t.sum; ans=t.ans;
}

void merge(int x,int y) { //合并两个点 
	x=get(x),y=get(y);
	sta[++top]=(rec){x,y,sz[x],sz[y],sum,ans};
	if(x == y) return ;
	if(x <= n && y > n) sum++,ans += (sz[x] == 1);//y>n的话,sz[y]=1.根据下面sz[x]<sz[y] swap得. 
	else ans -= (sz[x]>1);
	if(sz[x]<sz[y]) swap(x,y); 
	sz[x] += sz[y]; fa[y]=x; 
}

void upd(int x,int l,int r,int L,int R,int id) { 
	if(L<=l&&r<=R) return V[x].pb(id);//放入边. 
	int mid=(l+r)/2;
	if(L<=mid) upd(lc,l,mid,L,R,id);
	if(mid< R) upd(rc,mid+1,r,L,R,id);
}

void ask(int x,int l,int r) {
	int len=SZ(V[x]);
	for(int i=0;i<len;i++) {
		int id=V[x][i];
		merge(e[id].x,e[id].y);
	}
	if(l == r) {
		if(sum<m) puts("-1");
		else pr2(ans);
	} else {
		int mid=(l+r)/2;
		ask(lc,l,mid);
		ask(rc,mid+1,r);
	}
	while(len--) pop();//回撤 
}

int main() {
	qr(n); qr(m); qr(q);
	for(int i=1;i<=n+m;i++) fa[i]=i,sz[i]=1;
	for(int x=1,k,y;x<=n;x++) {
		qr(k); 
		while(k--) {
			qr(y); y+=n;
			S[mk(x,y)]=++tot;
			e[tot]=(E){x,y};
			L[tot]=1;
		}
	}
	for(int i=1,x,y;i<=q;i++) {
		qr(y); qr(x); y += n;
		if(!S[mk(x,y)]) {
			S[mk(x,y)]=++tot;
			e[tot]=(E){x,y};
		}
		int id=S[mk(x,y)];
		if(!L[id]) L[id]=i;
		else {
			if(i>1) upd(1,1,q,L[id],i-1,id);//[L[id],i)为作用时间. 
			L[id]=0;
		}
	}
	for(int i=1;i<=tot;i++) if(L[i]) upd(1,1,q,L[i],q,i);
	ask(1,1,q); return 0;
}

C Cinema

给定一个 n ∗ m n*m nm的矩形.用最少的十字覆盖所有位置,十字中心不能被别的十字覆盖.

状压dp.
定义 0 0 0表示未放置, 1 1 1被十字覆盖但不为中心, 2 2 2为十字中心.
我们可以先搜索出合法的状态,然后搜出合法的转移.
因为询问较多, m m m较小,我们必须要离线处理.

然后处理每个 m m m的答案.
记录 f [ n ] [ m ] f[n][m] f[n][m]为最小放置数量.
p r e [ n ] [ m ] pre[n][m] pre[n][m]为前驱状态.

扫到对应的 n n n时,我们直接把每一行的状态记录下来即可.

(细节有点多,调了很久…)

#include<bits/stdc++.h>
#define fi first
#define se second
#define lc (x<<1)
#define rc (x<<1|1)
#define gc getchar()//(p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++)
#define mk make_pair
#define pii pair<int,int>
#define pll pair<ll,ll>
#define pb push_back
#define IT iterator 
#define vi vector<int>
#define TP template<class o>
#define SZ(a) ((int)a.size())
#define all(a) a.begin(),a.end()
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const int N=1010,T=6666,size=1<<20,mod=998244353,inf=1e9;

//char buf[size],*p1=buf,*p2=buf;
template<class o> void qr(o &x) {
    char c=gc; x=0; int f=1;
    while(!isdigit(c)){if(c=='-')f=-1; c=gc;}
    while(isdigit(c)) x=x*10+c-'0',c=gc;
    x*=f;
}
template<class o> void qw(o x) {
    if(x/10) qw(x/10);
    putchar(x%10+'0');
}
template<class o> void pr1(o x) {
    if(x<0)x=-x,putchar('-');
    qw(x); putchar(' ');
}
template<class o> void pr2(o x) {
    if(x<0)x=-x,putchar('-');
    qw(x); putchar('\n');
}

int n,m,s[T],tot,c[T][3],f[N][T],pre[N][T],p[18];
struct rec {
    int s[N],n,m;
} q[N],tmp;
vector<pii> v[17];
vector<int> e[T];//transform

void solve(int x,int y) {
    while(x) {
        tmp.s[x]=s[y];
        y=pre[x--][y];
    }
}

int a[18],b[18];
short vis[15010000];

void dfs(int i) {//from 1
    if(i>m) {
        int x=0;
        a[0]=a[m+1]=0;
        for(int j=2;j<=m;j++) 
            if(a[j]==1&&a[j-1]==1) {
                if(a[j-2]!=2&&a[j+1]!=2) return ;
            }
        ++tot;
        for(int j=0;j<3;j++) c[tot][j]=0;
        for(int j=1;j<=m;j++) {
            x += a[j]*p[j-1];
            if(!vis[x]) vis[x]=-1;
            c[tot][a[j]]++;
        }
        s[tot]=x; vis[x]=tot;
        bool flag=1;
        for(int j=1;j<=m;j++)
            if(a[j]==1) {
                if(a[j-1]!=2&&a[j+1]!=2) {flag=0;break;}
            }
        if(flag) 
            f[1][tot]=c[tot][2];
        else f[1][tot]=inf;
        return ;
    }
    for(int j=0;j<3;j++) {
        a[i]=j;
        if(j&1) dfs(i+1);
        else a[i+1]=1,dfs(i+2);
    }
}

int now;
void div(int x) {
    for(int i=0;i<m;i++) b[i]=x%3,x /= 3;
}
void calc(int x,int i) {
    if(!vis[x]) return;
    if(i == m) {
        if(vis[x]<0) return;
        a[m]=0;
        for(int j=0;j<m;j++) 
            if(a[j]==1&&(!j||a[j-1]!=2)&&a[j+1]!=2&&b[j]!=2) return ;
        e[now].pb(vis[x]);
        return  ;
    }
    if(!b[i]) a[i]=2,calc(x+2*p[i],i+1);
    else if(b[i]==2) a[i]=1,calc(x+p[i],i+1);
    else for(int j=0;j<3;j++) a[i]=j,calc(x+p[i]*j,i+1);
}

int main() {
    int Q; qr(Q);
    for(int i=1,x,y;i<=Q;i++) {
        qr(x); qr(y);
        v[y].pb(mk(x,i));
    }
    p[0]=1; for(int i=1;i<=16;i++) p[i]=p[i-1]*3;
//    m=15; dfs(1); pr2(tot); 
//    for(int i=1;i<=tot;i++) div(s[i]),now=i,calc(0,0);
//    puts("ok");exit(0);
    for(m=1;m<=15;m++) if(SZ(v[m])) {
        tot=0; memset(vis,0,sizeof(short)*p[m]); vis[0]=-1;
        dfs(1);
        for(int i=1;i<=tot;i++) {
            now=i;
            e[i].clear();
            if(s[i]==48)
                i++,i--;
            div(s[i]),
            calc(0,0);
        }
        int pos=0; tmp.m=m;
        sort(all(v[m]));
        for(n=1;pos<SZ(v[m]);n++) {
            if(v[m][pos].fi == n) {
                int mn=inf,p=0;
                for(int i=1;i<=tot;i++)
                    if(!c[i][0] && f[n][i] < mn) 
                        mn=f[n][i], p=i;
                solve(n,p); tmp.n=n; tmp.s[0]=mn;
                while(pos<SZ(v[m]) && v[m][pos].fi == n) 
                    q[v[m][pos++].se]=tmp;
            }
            memset(f[n+1]+1,63,sizeof(int)*tot);
            for(int i=1;i<=tot;i++) if(f[n][i]<inf)
                for(int j:e[i]) {
                    int t=f[n][i]+c[j][2];
                    if(f[n+1][j]>t) {
                        f[n+1][j]=t;
                        pre[n+1][j]=i;
                    }
                }
        }
    }
    for(int i=1;i<=Q;i++)  {
        n=q[i].n; m=q[i].m;
        printf("Case #%d: %d\n",i,q[i].s[0]);
        for(int j=1;j<=n;j++) {
            int x=q[i].s[j];
            for(int k=0;k<m;k++) {
                if(x%3 == 2) putchar('*');
                else putchar('.');
//				putchar(x%3+'0');
                x /= 3;
            }
            puts("");
        }
    }
    return 0;
}

E Enigmatic Partition

n n n划分成 m m m个数 a 1 , a 2 . . . a m a_1,a_2...a_m a1,a2...am.
满足 a i ≤ a i + 1 ≤ a i + 1 , a m = a 1 + 2 a_i\le a_{i+1} \le a_i+1,a_m=a_1+2 aiai+1ai+1,am=a1+2.
求划分方案数.

参考博客: https://blog.youkuaiyun.com/qq_45458915/article/details/107777572?utm_medium=distribute.pc_relevant.none-task-blog-baidulandingword-1&spm=1001.2101.3001.4242

神仙差分题.
可以看出我们要用三段数表示 n n n.
设三段长度分别为 a , b , c a,b,c a,b,c.第一段的值为 x x x,则有:
a + b + c = m , x a + ( x + 1 ) ∗ b + ( x + 2 ) ∗ c = x m + b + 2 c = n a+b+c=m,xa+(x+1)*b+(x+2)*c=xm+b+2c=n a+b+c=m,xa+(x+1)b+(x+2)c=xm+b+2c=n.

我们观察一下,一个较小的情况:(蓝色表示 n n n)
在这里插入图片描述

我们把 x x x变为 x + 1 x+1 x+1的转移视作高度不变,

x + 1 x+1 x+1变为 x + 2 x+2 x+2的转移视作高度+1.

那么从 x , x , x . . . . ( x + 1 ) , ( x + 2 ) ( 111123 ) x,x,x....(x+1),(x+2)(111123) x,x,x....(x+1),(x+2)(111123)出发,每两个可以高度+1.(直到 123333 123333 123333).

于此同时,从 x , ( x + 1 ) , ( x + 2 ) , ( x + 2 ) . . . . ( 122223 ) x,(x+1),(x+2),(x+2)....(122223) x,(x+1),(x+2),(x+2)....(122223)出发,和每加一高度消失一个.(直到 123333 123333 123333后一个位置)

那么,在这个小情况看,就是 13 , 15 13,15 13,15的变化被抵消,也就是可以看作两个都是隔2变化.

此时令 s = a m s=am s=am.

定义差分数组 d d d.

d [ s + 3 ] + + ( 111123 ) , d [ s + m + 1 ] − − ( 222223 ) d[s+3]++(111123),d[s+m+1]--(222223) d[s+3]++(111123),d[s+m+1](222223).

d [ s + m + 2 ] − − ( 222233 ) , d [ s + 2 m − 3 + 3 ] + + ( 333333 ) d[s+m+2]--(222233),d[s+2m-3+3]++(333333) d[s+m+2](222233),d[s+2m3+3]++(333333).

然后隔项前缀和得到原本的差分值.

得到差分值 后 做二阶前缀和即可.


int T,n;
ll s[N];

int main() {
	qr(T); n=1e5;
	for(int m=3;m<=n;m++)
		for(int a=m;a<=n;a+=m) {
			s[a+3]++; s[a+m+1]--;
			s[a+m+2]--; s[a+2*m]++;
		}
	for(int i=2;i<=n;i++) s[i] += s[i-2];
	for(int i=1;i<=n;i++) s[i] += s[i-1];
	for(int i=1;i<=n;i++) s[i] += s[i-1];
	int l,r,num=0; 
	while(T--) printf("Case #%d: ",++num),qr(l),qr(r),pr2(s[r]-s[l-1]);
	return 0;
}

G Game SET

n n n个4元组,每个元组有4种可能.(有一种为任选).
如果三个元组的每一位要么全部相同要么全部不同,那么就合法.
尝试找出一个合法的三元组.

对于 ∗ * 的情况暴力枚举.
然后 n 2 n^2 n2枚举前两个,剩下一个可以用表查出.

int T,n,pos[3][3][3][3];
struct rec {
    int a[4];
}e;
vector<rec> a[N],tmp;
string s[4][4]={
    {"one","two","three","*"},
    {"diamond", "squiggle", "oval","*"},
    {"solid", "striped", "open","*"},
    {"red", "green", "purple","*"},
};
map<string,int> S[4];
char str[22];

void dfs(rec &a,int i=0) {
    if(i==4) return tmp.pb(a);
    if(a.a[i]==3) {
        for(int j=0;j<3;j++) a.a[i]=j,dfs(a,i+1);
        a.a[i]=3;
    }
    else dfs(a,i+1);
}

void get(char*p) {
	char c=gc;
	while(!(islower(c)||c=='*') ) c=gc;
	while(c!=']') *p++=c,c=gc;
	for(int i=0;i<10;i++) *p++=0;
}

int main() {
    for(int i=0;i<4;i++)
        for(int j=0;j<4;j++)
            S[i][s[i][j]]=j;
    int num=0;
    qr(T); while(T--) {
        qr(n); memset(pos,0,sizeof pos);
        for(int i=1;i<=n;i++) {
            tmp.clear();
            for(int j=0;j<4;j++) {
				get(str);
				e.a[j]=S[j][str];
			}
            dfs(e); a[i]=tmp;
            for(auto j:a[i])
                pos[j.a[0]][j.a[1]][j.a[2]][j.a[3]]=i;
        }
        printf("Case #%d: ",++num);
        bool flag=0;
        for(int i=1;i<n-1&&!flag;i++)
            for(int j=i+1;j<n&&!flag;j++)
                for(auto x:a[i]) {
                    for(auto y:a[j]) {
                       	rec z;
                       	for(int k=0;k<4;k++)
                       		if(x.a[k] == y.a[k]) z.a[k]=x.a[k];
                       		else z.a[k]=3-x.a[k]-y.a[k];
                    	int p=pos[z.a[0]][z.a[1]][z.a[2]][z.a[3]];
                    	if(p>j) {pr1(i); pr1(j); pr2(p); flag=1; break;}
                    }
                    if(flag) break;
                }
        if(!flag )puts("-1");
    }
    return 0;
}

H Hard String Problem

给定 n n n个串,求 n n n个的无穷循环串的公共子串有多少个.
无穷个则输出-1.

我们先用每个串的最小循环节的最小表示来替换原串.

然后对于两个串的情况,
一个引理是公共子串的长度不会>=3倍长串.
否则,可以推出更小的循环节.

所以我们令每个串都和最短的串求一下公共子串,取交即为答案.

#include<bits/stdc++.h>
#define fi first
#define se second
#define lc (x<<1)
#define rc (x<<1|1)
#define gc getchar()//(p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++)
#define mk make_pair
#define pii pair<int,int>
#define pll pair<ll,ll>
#define pb push_back
#define IT iterator 
#define vi vector<int>
#define TP template<class o>
#define SZ(a) ((int)a.size())
#define all(a) a.begin(),a.end()
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const int N=3e5+10,size=1<<20,mod=998244353,inf=2e9;

//char buf[size],*p1=buf,*p2=buf;
template<class o> void qr(o &x) {
	char c=gc; x=0; int f=1;
	while(!isdigit(c)){if(c=='-')f=-1; c=gc;}
	while(isdigit(c)) x=x*10+c-'0',c=gc;
	x*=f;
}
template<class o> void qw(o x) {
	if(x/10) qw(x/10);
	putchar(x%10+'0');
}
template<class o> void pr1(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); putchar(' ');
}
template<class o> void pr2(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); putchar('\n');
}

int n,ans,p[N];
string s[N];
char str[N*2];

int tot=1,last=1,now;
struct node{int v[26],fa,len,vis,t; } tr[N*16];
void add(int c) {
	int p=last,x=++tot; last=x; tr[x].len=tr[p].len+1;
	for( ;p&&!tr[p].v[c];p=tr[p].fa) tr[p].v[c]=x;
	if(!p) tr[x].fa=1;
	else {
		int q=tr[p].v[c],y;
		if(tr[p].len+1 == tr[q].len) tr[x].fa=q;
		else {
			tr[y=++tot]=tr[q];
			tr[y].len=tr[p].len+1;
			tr[q].fa=tr[x].fa=y;
			for( ;p&&tr[p].v[c] == q;p=tr[p].fa) tr[p].v[c]=y;
		}
	}
	while(x&&tr[x].vis^now) tr[x].vis=now,tr[x].t++,x=tr[x].fa;
}

int getmin(char *s) {//最小循环节
	int n=1;
	for(int i=2;s[i];i++) {
		n++; p[i]=p[i-1];
		while(p[i]&&s[p[i]+1]!=s[i]) p[i]=p[p[i]];
		p[i] += (s[p[i]+1] == s[i]);
	}
	int x=n;
	while(x) {
		x=p[x];
		if(n%(n-x)==0) return n-x;
	}
	return n;
}

string calc(char *s) {//最小表示法
	int t=getmin(s);
	for(int i=1;i<=t;i++) s[i+t]=s[i];
	int i=1,j=2,k;
	while(j<=t&&i<=t) {
		for(k=0;s[i+k]==s[j+k]&&k<t;k++);
		if(k == t) break;
		if(s[i+k]<s[j+k]) j += k + 1;
		else i += k + 1;
		if(i == j) i++;
	}
	i=min(i,j); s[i+t]=0;
	return string(s+i);
}

int LEN;
void debug(int x) {
	if(tr[x].t == n) {
		str[LEN+1]=0;pr1(x);
		puts(str+1);
	}
	for(int c=0;c<26;c++) 
		if(tr[x].v[c]) {
			str[++LEN]=c+'a';
			debug(tr[x].v[c]);
			LEN--;
		}
}

bool cmp(string &x,string &y) {
	return SZ(x)<SZ(y);
}

int main() {
	qr(n); 
	for(int i=1;i<=n;i++) {
		scanf("%s",str+1); 
		s[i]=calc(str);
	}
	bool flag=1;
	for(int i=2;i<=n;i++)
		if(s[i]!=s[i-1]) 
			{flag=0; break;}
	if(flag) puts("-1"),exit(0);
	sort(s+1,s+n+1,cmp);
	for(int i=2;i<=n;i++) {
		string t=s[i];
		s[i] += t+t+t;//公共子串长度<3倍长串,所以要用到4倍长串的长度.
	}
	string t=s[1];
	while(SZ(s[1])<SZ(s[n])) s[1] += t;
	for(int i=1;i<=n;i++) {
		last=1; now=i;
		for(char c:s[i]) add(c-'a');
	}
	ll ans=0;
	// debug(1);
	for(int i=2;i<=tot;i++)
		if(tr[i].t == n) 
			ans += tr[i].len-tr[tr[i].fa].len;
	pr2(ans); return 0;
}

I Interesting Computer Game

n n n个数对 ( a i , b i ) (a_i,b_i) (ai,bi).每次只能选一个数,问最后至多有多少个不同的数.

我们直接给两个数连边,然后答案就是总数字个数-连成树的连通块.

int T,n,fa[N],sz[N],e[N];
map<int,int> s; int tot;
int id(int x) {return !s[x]?s[x]=++tot:s[x];}
int get(int x) {return fa[x]==x?x:fa[x]=get(fa[x]); }

int main() {
    qr(T); while(T--) {
        qr(n); s.clear(); tot=0;
        for(int i=1;i<=n*2;i++) fa[i]=1,sz[i]=1,e[i]=0;
        for(int i=1,x,y;i<=n;i++) {
            qr(x);qr(y);
            x=get(id(x)); y=get(id(y));
            if(x==y) e[x]++;
            else {
                sz[y] += sz[x];
                e[y] += e[x] + 1;
                fa[x]=y;
            }
        }
        int ans=tot;
        pr2(tot);
        for(int i=1;i<=tot;i++) if(sz[i]==e[i]+1&&get(i)==i) ans--;
        pr2(ans);
    }
    return 0;
}

K Kabaleo Lite

n n n道菜(收益为 a i a_i ai,有 b i b_i bi个).
客人必须有菜,每种菜只能有一个,且菜的种类从1开始连续.
问最大客人数的情况下,最大收益为多少.

不难发现 b i b_i bi受限于 b i − 1 . . . b 1 b_{i-1}...b_1 bi1...b1,先前缀取个 min ⁡ \min min.
然后我们把菜都分给前面的人.
做一次收益的前缀和的最大值即可.
重点在于这题数据卡$long~long , 只 能 用 ,只能用 ,__ int128$/高精度卡过.

int T,n,num,a[N],b[N];
ll mx[N];

int main() {
    qr(T); b[0]=inf; 
    while(T--) {
        qr(n); __int128 ans=0;
        for(int i=1;i<=n;i++) qr(a[i]);
        for(int i=1;i<=n;i++) qr(b[i]),b[i]=min(b[i],b[i-1]);
        mx[0]=-1e18; ll s=0;
        for(int i=1;i<=n;i++) s+=a[i],mx[i]=max(mx[i-1],s);
        for(int i=1,pos=n;i<=b[1];i++) {
            while(b[pos]<i) pos--;
            ans += mx[pos];
        }
        printf("Case #%d: %d ",++num,b[1]); pr2(ans);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Infinite_Jerry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值