讀研以後好久沒有研究算法競賽的題目了。這段時間算法課,剛好作業是要講算法,就選了一道題目研究了一下,感覺還是蠻有意思的。但是說實話本弱雞太菜了,靠自己肯定解決不了這麼難的題,就參考了別人的代碼啦(這個也找了半天,有好多人的實現都沒看懂)。而且這個過程也接觸到了以前沒有接觸過的構圖技巧,叫什麼前後綴優化建圖,還是很有意義的。
題目鏈接(裏面B題):
https://codeforces.com/gym/101190
題意:
給定n個0/1串,每個串至多有一個問號。問你是否能夠找到方案,使得這些問號用0/1填充後,各個串之間互不爲前綴。如果是,輸出YES,並輸出方案。否則,輸出NO。
代碼主要參考:
https://blog.csdn.net/kzn2683331518/article/details/101431239
主要思路:
互不爲前綴,出現前綴這種字眼,可以聯想到字典樹。
對於問號的處理,用0替代插入字典樹,用1替代插入至字典樹中。
然後呢,替代字符串要麼取0要麼取1,對應於命題True/False,同時還有限制條件,即一個樹的分支上最多隻能有一個替代字符串,對分支上任意兩個串a、串b,有條件(not a) or (not b),這樣看就轉換成了2-sat問題。
關於字典樹,2-sat讀者可以知乎百度上搜搜了解下啦,或者也可以參考算法競賽入門經典訓練指南
(要是參加比賽的話私以爲2-sat可能出現比較少,但裏面用到的tarjan算法求有向圖強聯通分量這個更重要更普遍點,讀者最好理解這個算法)
然而事情還沒有結束。考慮極端情形,如果n個串在一個分支上,這樣加限制條件的話,限制條件數就有n的平方級別個,太多了,這個時候就有一種叫前後綴優化的東西~~~(點到爲止,主要看代碼註釋)
下面就是自己參考了別人代碼,寫了一遍,加了一點註釋的結果:
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e6+10;
vector<int> G[maxn];
//loc[node]存儲替代問號以後,字符串終結點在node上的字符串序號
vector<int> loc[maxn];
int sumn,n;
int no[maxn],lowlink[maxn],sccno[maxn],dfs_block,scc_cnt;
int wh[maxn],pos[maxn];
int ch[maxn][2],father[maxn],tot;
string str[maxn];
stack<int> S;
//加邊u->v,即若u則必須v
void add_clause(int u,int v){
G[u].push_back(v);
}
void link(int u,int v){
add_clause(u,v);
add_clause(v^1,u^1); //逆否命題
}
//tarjan算法中深度優先遍歷部分
void dfs(int u) {
no[u]=lowlink[u]=++dfs_block;
S.push(u);
for (int i=0;i<G[u].size();i++) {
int v=G[u][i];
if (!no[v]) {
dfs(v);
lowlink[u]=min(lowlink[u],lowlink[v]);
} else if (!sccno[v]) {
lowlink[u]=min(lowlink[u],no[v]);
}
}
if (lowlink[u]==no[u]) {
scc_cnt++;
for (;;) {
int x=S.top();
S.pop();
sccno[x]=scc_cnt;
if (x==u)
break;
}
}
}
void two_sat_solve(int sumn,int n) {
for (int i=0;i<=sumn;i++) {
if (!no[i])
dfs(i);
}
//sccno[x]表示x屬於的強聯通分量號
for (int i=1;i<=n;i++)
if (sccno[i<<1]==sccno[i<<1|1]) {
puts("NO");
return;
}
puts("YES");
for (int i=1;i<=n;i++) {
//wh[i]表示字符串i中問號出現的位置,值爲-1則未出現
if (wh[i]!=-1)
str[i][wh[i]]=(sccno[i<<1]<sccno[i<<1|1]?'0':'1');
puts(str[i].c_str());
}
}
//利用字典樹插入,返回各替代問號以後字符串終結點在樹中序號
int trie_insert(const char *s) {
int u=0;
for (int i=0;s[i];i++) {
int c=s[i]^48; //根據s[i]爲'0'或'1'獲取c值0或1
if (!ch[u][c]) {
ch[u][c]=++tot;
father[tot]=u;
}
u=ch[u][c];
}
return u;
}
//處理輸入
void dealInput(){
freopen("binary.in","r",stdin);
freopen("binary.out","w",stdout);
cin>>n;
for (int i=1;i<=n;i++){
cin>>str[i];
int whpos=-1;
int sz=str[i].size();
for (int j=0;j<sz;j++)
if (str[i][j]=='?'){
whpos=j;
break;
}
//第一種情況:字符串中沒有問號(也抽象成一個用0一個用1,但是終結點還是對應原字符串插入終結位置)
if (whpos==-1) {
pos[i<<1]=pos[i<<1|1]=trie_insert(str[i].c_str());
add_clause(i<<1|1,i<<1);
}
//第二種情況:字符串中有問號
else {
//用0替代
str[i][whpos]='0';
pos[i<<1]=trie_insert(str[i].c_str());
//用1替代
str[i][whpos]='1';
pos[i<<1|1]=trie_insert(str[i].c_str());
}
wh[i]=whpos;
}
}
//結束輸入處理後進行加邊操作,返回總結點數
int deal_edge(){
int s=(n<<1|1)+1;
//樹上所有點node,若從根到father[node]上選1個替代字符串點,則從根到node選1個替代字符串點
for (int node=1;node<=tot;node++)
link(s+(father[node]<<1),s+(node<<1));
for (int i=1;i<=n;i++) {
int tnode0=pos[i<<1];
int tnode1=pos[i<<1|1];
if (wh[i]==-1) {
loc[tnode0].push_back(i<<1);
link(i<<1,s+(father[tnode0]<<1|1)); //字符串i用0替代問號,root~father[tnode0]上不能再有替代字符串點
link(i<<1,s+(tnode0<<1)); //字符串i用0替代問號,root~tnode0上有1個替代字符串點
}else{
loc[tnode0].push_back(i<<1);
link(i<<1,s+(father[tnode0]<<1|1));
link(i<<1,s+(tnode0<<1));
loc[tnode1].push_back(i<<1|1); //用1替代,情況類似
link(i<<1|1,s+(father[tnode1]<<1|1));
link(i<<1|1,s+(tnode1<<1));
}
}
s+=(tot<<1|1)+1;
for (int node=1;node<=tot;node++) {
//noden存儲node上替代字符串點的數目,顯然根據題意只能從裏面選1個
int noden=loc[node].size();
//node上沒有替代字符串點,不處理
if (!noden)
continue;
for (int i=0;i<noden;i++){
if (i) {
link(s+(i-1<<1),s+(i<<1)); //這個node上前i-1個裏選了1個,則前i個裏選1個
link(loc[node][i],s+((i-1<<1)|1)); //這個node上選了第i個點,前i-1個裏面不能再選
}
link(loc[node][i],s+(i<<1)); //這個node上選了第i個點,則前i個裏選1個
}
s+=noden<<1;
}
return s;
}
int main() {
dealInput();
int sumn=deal_edge();
two_sat_solve(sumn,n);
return 0;
}