[NOI2017]遊戲「2-SAT」
[NOI2017]遊戲「2-SAT」
題目描述
小 L 計劃進行 \(n\) 場遊戲,每場遊戲使用一張地圖,小 L 會選擇一輛車在該地圖上完成遊戲。
小 L 的賽車有三輛,分別用大寫字母 \(A\)、\(B\)、\(C\) 表示。地圖一共有四種,分別用小寫字母 \(x\)、\(a\)、\(b\)、\(c\) 表示。
其中,賽車 \(A\) 不適合在地圖 \(a\) 上使用,賽車 \(B\) 不適合在地圖 \(b\) 上使用,賽車 \(C\) 不適合在地圖 \(c\) 上使用,而地圖 \(x\) 則適合所有賽車參加。
適合所有賽車參加的地圖並不多見,最多隻會有 \(d\) 張。
\(n\) 場遊戲的地圖可以用一個小寫字母組成的字串描述。例如:\(S=\texttt{xaabxcbc}\)
小 L 對遊戲有一些特殊的要求,這些要求可以用四元組 \((i, h_i, j, h_j)\) 來描述,表示若在第 $i4 場使用型號為 \(h_i\) 的車子,則第 \(j\) 場遊戲要使用型號為 \(h_j\) 的車子。
你能幫小 L 選擇每場遊戲使用的賽車嗎?如果有多種方案,輸出任意一種方案。
如果無解,輸出 -1
。
輸入格式
輸入第一行包含兩個非負整數 \(n\), \(d\)。
輸入第二行為一個字串 \(S\)。
\(n\), \(d\), \(S\) 的含義見題目描述,其中 \(S\) 包含 \(n\) 個字元,且其中恰好 \(d\) 個為小寫字母 \(x\)。
輸入第三行為一個正整數 \(m\) ,表示有 \(m\) 條用車規則。
接下來 \(m\) 行,每行包含一個四元組 \(i,h_i,j,h_j\) ,其中 \(i,j\) 為整數,\(h_i,h_j\) 為字元 \(A\) 、\(B\) 或 \(C\)
輸出格式
輸出一行。
若無解輸出 -1
。
若有解,則包含一個長度為 \(n\) 的僅包含大寫字母 \(A\)、\(B\)、\(C\) 的字串,表示小 L 在這 \(n\) 場遊戲中如何安排賽車的使用。如果存在多組解,輸出其中任意一組即可。
輸入輸出樣例
輸入 #1
3 1
xcc
1
1 A 2 B
輸出 #1
ABA
思路分析
來,今天我們學習\(2-SAT\),首先從這個黑題入手
- 每個地圖都有兩個或三個狀態可選,另加了一些限制,顯然是一道 \(2—SAT\) 的題
- 關鍵在於對於 \(x\) 地圖,有三種狀態,難不成弄個 \(3—SAT\) ?然而這個做法已被證明是 \(NPC\)
- 為了變成 \(2-SAT\) 問題,我們將 $x $地圖分類討論,第一種是不能選 \(A\) ,第二種不能選 \(B\),這時候選 \(A\)、\(B\)、\(C\) 的情況就都已經囊括了,不須再進一步列舉
- 接下來是連邊,對於每個四元組 \(i,h_i,j,h_j\):
- \(h_i\) 是不可選的,不需連邊,跳過即可
- \(h_j\) 是不可選的,則需要建邊強制i不能選 \(h_i\)
- 都可選,直接連邊,表示同時選,另外再根據逆否命題建一個反向邊,即同時不選
細節真的超多
\(Code\)
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define N 500100
using namespace std;
inline int read(){
int x = 0,f = 1;
char ch = getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int n,d,m,dfs_clock,scc_cnt,dfn[N],low[N],head[N],sta[N],top,belong[N],in[N],a[N],b[N],gn[N];
char s[N],h1[N],h2[N],ans[N];
struct edge{
int to,next;
}e[N<<2];
int len;
void addedge(int u,int v){
e[++len].to = v;
e[len].next = head[u];
head[u] = len;
}
void Init(){ //每次改變x圖的狀態都要重新建圖
memset(head,0,sizeof(head));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(belong,0,sizeof(belong));
memset(in,0,sizeof(in));
memset(sta,0,sizeof(sta));
dfs_clock = scc_cnt = top = len = 0;
}
void Tarjan(int u){ //Tarjan縮點
dfn[u] = low[u] = ++dfs_clock;
sta[++top] = u;
in[u] = 1;
for(int i = head[u];i;i=e[i].next){
int v = e[i].to;
if(!dfn[v]){
Tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(!belong[v])low[u] = min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
scc_cnt++;
while(1){
int x = sta[top--];
belong[x] = scc_cnt;
in[x] = 0;
if(u==x)break;
}
}
}
bool check(){//尋找可行方案
for(int i = 1;i <= (n<<1);i++)if(!dfn[i])Tarjan(i);
for(int i = 1;i <= n;i++){
if(belong[i]==belong[i+n])return false; //兩個狀態出現在了一個scc裡,就沒有合理方案
if(belong[i]<belong[i+n])ans[i] = (s[i]=='A') ? 'B' : 'A'; //根據字典序優先,第一個狀態只能是A或C
else ans[i] = (s[i]=='C') ? 'B' : 'C'; //同理
}
for(int i = 1;i <= n;i++)putchar(ans[i]); //找到一種方案輸出就行了
return true;
}
int main(){
n = read(),d = read();
d = 0;
scanf("%s",s+1);
m = read();
for(int i = 1;i <= n;i++)if((s[i]-=32)&&s[i]=='X')gn[++d] = i; //x圖儲存起來
for(int i = 1;i <= m;i++){
a[i] = read();scanf(" %c",&h1[i]);
b[i] = read();scanf(" %c",&h2[i]);
}
bool flag = 0;
for(int i = 0;i <= (1<<d)-1;i++){ //列舉2^d個狀態,這裡用了2進位制位,每個x圖相應的2進位制位為1就不能A,否則不能選B
Init();
for(int j = 1;j <= d;j++)s[gn[j]] = (i&(1<<(j-1))) ? 'A' : 'B';
for(int j = 1;j <= m;j++){
if(h1[j] == s[a[j]])continue;
if(h2[j] == s[b[j]]){
if(h1[j]=='C' || (h1[j] =='B'&&s[a[j]]=='C'))addedge(a[j]+n,a[j]); //這裡的if是根據字典序大小而進行連邊操作
else addedge(a[j],a[j]+n);
continue;
}
int add1,add2;
if(h1[j] == 'C' || (h1[j] == 'B' && s[a[j]] == 'C')) add1 = n;
else add1 = 0;
if(h2[j] == 'C' || (h2[j] == 'B' && s[b[j]] == 'C')) add2 = n;
else add2 = 0;
addedge(a[j] + add1, b[j] + add2);
addedge(b[j] - add2 + n, a[j] - add1 + n); //反向邊,巧妙處理
}
if(check()){flag = 1;break;}
}
if(!flag)printf("-1");
return 0;
}