I-Choose a Name_2026 题解——牛客暑期多校训练营10

I-Choose a Name_2026牛客暑期多校训练营10

\(n\) 个用户,第 \(i\) 个用户有两个候选字符串 \(l_i\)\(r_i\)。需为每个用户恰选一个,使得所选 \(n\) 个字符串两两不互为前缀(即前缀无关)。判断可行性,若可行输出任意方案(字符 L/R)。\(\sum n\le5\times10^5\),所有字符串总长 \(\le10^6\)

可以说是一道 2-sat 的好题了,难点在于如何把原本 \(O(n^2)\) 的关系转化为线性。

首先先建一颗字典树(trie树),相同的字符串对应树上一个节点,父亲节点相当于前缀。

只需要考虑一下三种关系

  1. 每个用户的两个候选串中,至多选择一个。
  2. 字典树中,相同节点内的多个字符串至多选择一个。
  3. 字典树中,如果祖先节点的字符串被选取,则该节点不能选取。

关系一

对于第一种关系,就是简单的布尔变量,定义第 \(i\) 个用户命题为 \(x_i\)\(x_i\) 为真即表示选择 \(l_i\)\(x_i\) 为假则表示选择 \(r_i\)。如果最后真命题和假命题处于同一个强连通分量则无解。

关系二(重点,可作为模板)

对于第二种关系是本题的核心。我们假设一个节点共对应 \(m\) 个字符串,它们为真的命题分别为 \(a_1,a_2,\dots,a_m\)。在定义一个类似前缀和的东西 \(s\)\(s_i\) 表示前 \(i\) 个字符串至少选择了一个为真。它们的否命题记作 \(\neg a_i,\neg s_i\)

则我们可以构建以下的关系(后面备注了建图,方便用推导的方式来考虑问题,即 \(x\to y\) 表示 \(x\) 可以推导出 \(y\)

  • \(a_i\) 或者 \(\neg s_i\)\(a_i\to s_i,\neg s_i\to \neg a_i\)
  • \(s_i\) 或者 \(\neg s_{i+1}\)\(s_i\to s_{i+1},\neg s_{i+1}\to \neg s_{i}\)
  • \(s_i\) 或者 \(a_{i+1}\)\(s_i\to \neg a_{i+1},a_{i+1}\to \neg s_i\)

通过这种模式我们实现了相同的字符串只选择一个,且 \(s_m\) 则表示该字典树节点对应的字符串中有一个被选择。

容易发现这样建图是 \(O(n)\) 的。

关系三(有点妙)

有了解决关系二的成果,解决关系三我们只需要使用之前求得的 \(s_m\),将 \(s_m\) 换个名字为 \(b\)\(b_u\) 表示字典树 \(u\) 节点对应的字符串之一被选择,相应的有 \(\neg b_u\),且祖先节点的 \(b\) 为真则该节点的 \(b\) 应该为假。

当然可以仿照我们关系二的做法,定义一个树上前缀和 \(pre_u\) 表示 \(u\) 到根节点中的字符串中有一个被选,令 \(f\) 为父亲节点编号。则相似地有:\(b_u\) 或者 \(\neg pre_u\)\(pre_f\) 或者 \(\neg pre _u\)\(pre_f\) 或者 \(b_u\)

int pre[N];
void dfs2(int u,int f){
	if(f!=-1){
		Two_SAT::addedge(blk[u]*2+1,pre[u]*2);
		Two_SAT::addedge(pre[f]*2+1,pre[u]*2);
		Two_SAT::addedge(pre[f]*2+1,blk[u]*2+1);
	}
	for(int c=0;c<26;++c){
		if(ch[u][c])
			dfs2(ch[u][c],u);
	}
}

但实际上可以直接将子节点与所有的祖先节点建立关系:\(b_u\) 或者 \(b_{ant}\)

int stk[N],top;//记录一个节点的祖先节点命题集合
void dfs(int u){
	if(blk[u]){
		for(int i=1;i<=top;++i){
			Two_SAT::addedge(stk[i]*2+1,blk[u]*2+1);//如果祖先节点选了则该节点不能选
		}
		stk[++top]=blk[u];
	}
	for(int c=0;c<26;++c){
		if(ch[u][c])
			dfs(ch[u][c]);
	}
	if(blk[u])--top;
}

这样的复杂度为什么是对的,因为对于长度为 \(len\) 的字符串,他至多与 \(len-1\) 个字典树节点建立关系,由于字符串总长度是 \(n\) 量级,因此关系总数也是 \(O(n)\) 的。当然这样是对的需要保证我们将所有相同的字符串只选一个变成命题 \(s_m\)

以下是完整代码,装模做样封装了一个 2-sat。

#include <bits/stdc++.h>

using namespace std;


typedef long long ll;
typedef pair<int,int>ttfa;
const int N=1000006;
const ll INF=0x3f3f3f3f3f3f3f3f;


namespace Two_SAT{
	const int NN=4*N;
	int nn;
	int dfn[NN],low[NN],tim,stk[NN],top,tot,col[NN],vis[NN],ans[NN];
	vector<int>tar[NN];
	void clear(){
		tim=top=tot=0;
		for(int i=0;i<=2*nn+1;++i){//inner n
			dfn[i]=low[i]=stk[i]=col[i]=vis[i]=0;
			tar[i].clear();
		}
		nn=0;
	}
	void tarjan(int u){
		dfn[u]=low[u]=++tim;
		stk[++top]=u;vis[u]=1;
		for(auto v:tar[u]){
			if(!dfn[v]){
				tarjan(v);
				low[u]=min(low[u],low[v]);
			}else if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
		if(dfn[u]==low[u]){
			++tot;
			do{
				col[u]=tot;
				u=stk[top--];
				vis[u]=0;
			}while(dfn[u]!=low[u]);
		}
	}
	bool solve(){
		for(int i=1;i<=2*nn+1;++i){//inner n
			if(!dfn[i])tarjan(i);
		}
		for(int i=1;i<=nn;++i){//此处定义 2*i 为 i 命题 Flase
			if(col[2*i]==col[2*i+1]){
				return 0;
			}
			if(col[2*i]<col[2*i+1])ans[i]=0;
			else ans[i]=1;
		}
		return 1;
	}
	void addedge(int u,int v){//u 或 v
		// printf("addedge %d %d\n",u,v);
		// printf(" addline %d %d\n",u,v^1);
		// printf(" addline %d %d\n",v,u^1);
		tar[u].push_back(v^1);
		tar[v].push_back(u^1);
	}
	int group_ban(vector<int>arr){
		// printf("group_ban: ");
		// for(auto x:arr)printf("%d ",x);
		// puts("");
		int m=(int)arr.size();
		for(int i=0;i<m;++i){
			addedge(arr[i],(nn+i+1)*2);
			if(i>0){
				addedge((nn+i)*2+1,(nn+i+1)*2);
				addedge((nn+i)*2+1,arr[i]);
			}
		}
		nn+=m;
		return nn;
	}
}
int n;
int ch[N][26],tot,blk[N];//blk 为 trie 相同字符串的总集
int idx[N][2];//字符串对应trie树节点
vector<int>lis[N];//trie树上挂的命题编号
char str[N];


int inser(char *s){
	int m=strlen(s+1);
	int u=0;
	for(int i=1;i<=m;++i){
		int c=s[i]-'a';
		if(!ch[u][c])ch[u][c]=++tot;
		u=ch[u][c];
	}
	return u;
}

int stk[N],top;//记录一个节点的祖先节点命题集合
void dfs(int u){
	if(blk[u]){
		for(int i=1;i<=top;++i){
			Two_SAT::addedge(stk[i]*2+1,blk[u]*2+1);//如果祖先节点选了则该节点不能选
		}
		stk[++top]=blk[u];
	}
	for(int c=0;c<26;++c){
		if(ch[u][c])
			dfs(ch[u][c]);
	}
	if(blk[u])--top;
}
int pre[N];
void dfs2(int u,int f){
	if(f!=-1){
		Two_SAT::addedge(blk[u]*2+1,pre[u]*2);
		Two_SAT::addedge(pre[f]*2+1,pre[u]*2);
		Two_SAT::addedge(pre[f]*2+1,blk[u]*2+1);
	}
	for(int c=0;c<26;++c){
		if(ch[u][c])
			dfs2(ch[u][c],u);
	}
}

int main(){
	int Test;scanf("%d",&Test);
	while(Test--){
		scanf("%d",&n);
		for(int i=1;i<=n;++i){
			scanf("%s",str+1);
			idx[i][0]=inser(str);
			lis[idx[i][0]].push_back(i*2);
			scanf("%s",str+1);
			idx[i][1]=inser(str);
			lis[idx[i][1]].push_back(i*2+1);
			//printf("%d %d\n",idx[i][0],idx[i][1]);
		}
		Two_SAT::nn+=n;
		for(int i=1;i<=tot;++i){
			if(lis[i].size()){
				blk[i]=Two_SAT::group_ban(lis[i]);
			}
		}
		// 方法一:直接祖先建边
		// dfs(0);
		// 方法二:“前缀和”建边
		for(int i=0;i<=tot;++i){
			pre[i]=++Two_SAT::nn;
		}
		dfs2(0,-1);

		bool flag=Two_SAT::solve();
		if(!flag){puts("NO");}
		else{
			puts("YES");
			for(int i=1;i<=n;++i){
				if(Two_SAT::ans[i]==0)putchar('l');
				else putchar('r');
			}puts("");
		}
		Two_SAT::clear();
		for(int i=0;i<=tot;++i){
			blk[i]=0;
			for(int j=0;j<26;++j)
				ch[i][j]=0;
			lis[i].clear();
		}
		tot=0;
	}
	return 0;
}
posted @ 2026-09-08 23:39  BigSmall_En  阅读(5)  评论(0)    收藏  举报