1. 程式人生 > 遊戲 >Spencer不想讓Xbox團隊複製競爭對手的獨佔策略

Spencer不想讓Xbox團隊複製競爭對手的獨佔策略

1. \(\rm 2-SAT\) 問題簡述

\(n\) 個變數,每個變數有隻有 \(2\) 種取值,還有 \(m\) 個約束條件,每個條件都是對 \(k\) 個變數的約束。問這 \(n\)​​ 個變數有沒有一種取值方法,能滿足這 \(m\) 個條件,這個問題就是 \(\rm k-SAT\) 問題,其中 \(\text{SAT}\)\(\text{satisfiability}\) 的縮寫,意為“滿足性”​。

\(k>2\) 時,\(\rm k-SAT\) 問題為 \(\rm NP\) 完全問題,只能用暴力;當 \(k=2\) 時,我們可以通過 強連通分量(我用了 \(\rm Tarjan\)

) 實現 \(\operatorname{O}(n+m)\)​ 解決。

舉個栗子:

現在舉行了一場 \(\left\lceil 資料刪除\right\rfloor\)​​ 的比賽,有 \(3\)​​​ 位候選人和 \(3\)​​​ 位評委,每位評委要滿足條件之一:

  1. yzh 評委:
    • cxr 進入決賽;
    • wsy 進入決賽。
  2. xhj 評委:
    • wsy 進入決賽;
    • zlq 不進入決賽。
  3. sid 評委:
    • zlq 進入決賽;
    • cxr 進入決賽。

那麼我們可以找到一組方案:cxr 不進入決賽,wsy 進入決賽,zlq 進入決賽(完了我又要被揍了啊 /fad)。

2. \(\rm 2-SAT\)
​ 問題解決

P4782 【模板】2-SAT 問題

題意

\(n\) 個變數 \(x_1\sim x_n(x_i\in\{0,1\})\),另有 \(m\) 個需要滿足的條件,每個條件給出 \(i,a,j,b\),表示 \(\lceil x_i\)\(a\)\(x_j\)\(b\rfloor\)。給每個變數賦值使得所有條件得到滿足,若無解,輸出 IMPOSSIBLE,否則輸出 POSSIBLE 並構造一組解。

思路

先建立有 \(2n\) 個節點的有向圖,第 \(i\) 號節點意味著 \(x_i=0\),第 \(i+n\) 號節點意味著 \(x_i=1\)

對於一個約束條件:

  1. \(a=0,b=0\)​​​​​​,則向 \(i+n\to j\)​​​​​​ 連邊,\(j+n\to i\)​​​​​​ 連邊,說明當 \(x_i=1\)​​​​​​ 時 \(x_j\)​​​​​​ 必須取 \(0\)​​​​​​,\(x_j=1\)\(x_i\) 必須取 \(0\)​​​​​​​​;
  2. \(a=0,b=1\)​​​​​​,則向 \(i+n\to j+n\)​​​​​​ 連邊,\(j\to i\)​​​​​​ 連邊,說明當 \(x_i=1\)​​​​​​ 時 \(x_j\)​​​​​​ 必須取 \(1\)​​​​​​,\(x_j=0\)\(x_i\) 必須取 \(0\)​​​​​​​;
  3. \(a=1,b=0\)​​​​​​​​,則向 \(i\to j\)​​​​​ 連邊,\(j+n\to i+n\)​​​​​ 連邊,說明當 \(x_i=0\)​​​​​ 時 \(x_j\)​​​​​ 必須取 \(0\)​​​​​,\(x_j=1\)\(x_i\) 必須取 \(1\)​​​​​​​​​;
  4. \(a=1,b=1\)​​​​​​​​​,則向 \(i\to j+n\)​​​​​​​​​ 連邊,\(j\to i+n\)​​​​​​​​​ 連邊,說明當 \(x_i=0\)​​​​​​​​​ 時 \(x_j\)​​​​​​​​​ 必須取 \(1\)​​​​​​​​​,\(x_j=0\)​​​​​​ 時 \(x_i\)​​​​​​ 必須取 \(1\)​​​​​​​​​。

建圖程式碼:

while (m--)
{
    int i, a, j, b;
    scanf("%d%d%d%d", &i, &a, &j, &b);
    if (a == 0)
    {
        if (b == 0)
        {
            add(i + n, j);
            add(j + n, i);
        }
        else
        {
            add(i + n, j + n);
            add(j, i);
        }
    }
    else
    {
        if (b == 0)
        {
            add(i, j);
            add(j + n, i + n);
        }
        else
        {
            add(i, j + n);
            add(j, i + n);
        }
    }
}

當然,我們可以簡化一下:

while (m--)
{
    int i, a, j, b;
    scanf("%d%d%d%d", &i, &a, &j, &b);
    add(i + a * n, j + (1 - b) * n);
    add(j + b * n, i + (1 - a) * n);
}

建圖後,我們求一遍強連通,設點 \(i\) 所在的強連通的編號為 \(c_i\),遍歷 \(i=1\to n\),然後判斷:若 \(c_i=c_{i+n}\):說明若 \(x_i\)\(0/1\),則對應的,\(x_i\) 必須取 \(1/0\)​​???炸了,所以我們推出了矛盾,即無解。

否則說明有解,那麼我們要怎麼構造解呢?

其實直接取所在強連通的編號更小的那個即可,原因如下:

在用 \(\rm Tarjan\) 求強連通時,由於是往下搜,所以實際上更晚訪問的強連通會被先標記,即該強連通的編號更小。

對於一個節點 \(i\),假設它對應的是取 \(0\),則取 \(1\) 的是 \(i+n\),若有這樣一條路

\[i\to j\to i+n \]

那麼 \(i+n\) 所在的強連通編號更小。當我們取 \(i\)​ 時同時會取到 \(i+n\),就不行了,所以我們只能取 \(i+n\)​​ 所在的強連通,即編號更小的。

for (int i = 1; i <= n; i++)
{
    printf("%d ", c[i] < c[i + n]);
}

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;

const int MAXN = 2e6 + 5;

int cnt, Time, scc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
bool ins[MAXN];
stack<int> s;

struct edge
{
	int to, nxt;
}e[MAXN << 1];

void add(int u, int v) 
{
	e[++cnt] = edge{v, head[u]};
	head[u] = cnt;
}

void tarjan(int u)
{
	dfn[u] = low[u] = ++Time;
	s.push(u);
	ins[u] = true;
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if (ins[v])
		{
			low[u] = min(low[u], dfn[v]);
		}
	}
	if (dfn[u] == low[u])
	{
		scc++;
		int v = 0;
		while (v != u)
		{
			v = s.top();
			s.pop();
			c[v] = scc;
			ins[v] = false;
		}
	}
}

int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++)
	{
		int u, a, v, b;
		scanf("%d%d%d%d", &u, &a, &v, &b);
		add(u + a * n, v + (1 - b) * n);
		add(v + b * n, u + (1 - a) * n);
	}
	for (int i = 1; i <= (n << 1); i++)
	{
		if (!dfn[i])
		{
			tarjan(i);
		}
	}
	for (int i = 1; i <= n; i++)
	{
		if (c[i] == c[i + n])
		{
			puts("IMPOSSIBLE");
			return 0; 
		}
	}
	puts("POSSIBLE");
	for (int i = 1; i <= n; i++)
	{
		printf("%d ", c[i] < c[i + n]);
	}
	return 0;
}