【题目总结】2-SAT

2-SAT简述

2-SAT,简单说就是给出 n n n个集合,每个集合里面有两个元素,已知有若干个 ⟨ a , b ⟩ \langle a,b \rangle a,b,表示 a a a b b b互相矛盾(即 a a a b b b不会在同一个集合当中)。现在从每一个集合之中选择一个元素,判断能否一共选 n n n个两两不相矛盾的元素。显然可能有多种选择方案,但是一般我们只需要找出有或没有。

2-SAT问题的解决方法是:假设有 ⟨ a 1 , b 1 ⟩ \langle a_1,b_1\rangle a1,b1,表示选了 a 1 a_1 a1就不能选 a 2 a_2 a2,选了 a 2 a_2 a2就不能选择 a 1 a_1 a1。也就是说对于每一个 a i a_i ai,其都有两种状态,一种是被选择,一种是没有被选择。我们想要同时描述这两种状态,可以采取将这两种状态当成两个点来考虑。

我一般习惯于用将第 i i i个元素被选中的状态编号为 i i i,没有被选中的状态编号为 i + n i+n i+n,其中 n n n为元素的个数。可以发现,这种编号方式不会冲突。

同样可行的方式是对于第 i i i个元素,被选中的状态编号为 2 i 2i 2i,没有被选中的状态编号为 2 i + 1 2i+1 2i+1。这样如果 x x x表示第 i i i个元素被选择,那么 x ⊕ 1 x \oplus1 x1表示没有被选择;如果 x x x表示第 i i i个元素没有被选择,那么 x ⊕ 1 x\oplus 1 x1表示已经被选择。(此处 ⊕ \oplus 表示异或运算)

那么对于每一个“选了 a a a就不能选 b b b”,都可以理解为主动选择 a a a被选中的状态后,就必须选择 b b b不被选中的状态。换句话说,我们要从主动向被动连边。

对于每一句话:“ a a a b b b只能去一个”,可以拆成两句话:选了 a a a就不能选 b b b,选了 b b b就不能选 a a a

按照题目要求建好图之后,我们利用强联通分量来解决这个问题。
我们通过判断一个集合中的两个元素是否在同一个强连通分量中,如果存在这样的情况,那么就没有一种方案可以满足题目的条件。

时间复杂度 O ( n + m ) O(n+m) O(n+m)

LuoguP4782 - 【模板】2-SAT问题

添加链接描述

题目中很直接地给出了2-SAT的条件。难点在于如何输出方案。我们知道,如果两个状态存在于同一个强联通分量,那么这两个状态是一定选择了其中一个就要选择另外一个的关系。在我们利用Tarjan算法求强联通分量时使用了栈,所以Tarjan求得的强联通的编号是一种反拓扑序。

利用这个性质,我们可以通过变量在途中的拓扑序来确定这个变量的取值(被选择/不被选择)。如果 x x x的编号在 ! x !x !x的前面,那么 x x x为真,否则选取 x x x为假。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000005;

int n, m, en, sccCnt = 0;
int front[N * 2], stk[N * 2], ins[N * 2], low[N * 2], dfn[N * 2];
int color[N * 2];
int di, si;

struct Edge {
    
    
	int v, next;
}e[N * 4];

void addEdge(int u, int v) {
    
    
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan(int x) {
    
    
	dfn[x] = low[x] = ++di;
	stk[++si] = x; ins[x] = 1;
	for (int i = front[x]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (!dfn[v]) {
    
    
			tarjan(v);
			low[x] = min(low[x], low[v]);
		}
		else if (ins[v]) {
    
    
			low[x] = min(low[x], dfn[v]);
		}
	}
	if (dfn[x] == low[x]) {
    
    
		++sccCnt;
		do {
    
    
			color[x] = sccCnt;
			x = stk[si--];
			ins[x] = 0;
		} while (low[x] != dfn[x]);
	}
}

void main2() {
    
    
	cin >> n >> m;
	en = di = si = sccCnt = 0;
	for (int i = 1; i <= n; ++i) {
    
    
		low[i] = dfn[i] = front[i] = 0;
	}
	for (int i = 1; i <= m; ++i) {
    
    
		int x, y, tx, ty;
		cin >> x >> tx >> y >> ty;
		addEdge(x + n * (tx ^ 1), y + n * ty);
		addEdge(y + n * (ty ^ 1), x + n * tx);
	}
	for (int i = 1; i <= n * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		if (color[i] == color[i + n]) {
    
    
			cout << "IMPOSSIBLE\n";
			return;
		}
	}
	cout << "POSSIBLE\n";
	for (int i = 1; i <= n; ++i) {
    
    
		if (color[i] > color[i + n]) cout << 1 << ' ';
		else cout << 0 << ' ';
	}
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
//	cin >> _;
	while (_--) main2();
	return 0;
}

HDU3062 - Party

题目链接

2-SAT经典题目。

每一对夫妇只能去一个人,所以去丈夫还是去妻子的问题,可以变成丈夫去或者不去的问题。于是就变成了 n n n个丈夫去或者不去,这样丈夫不去就代表妻子去。

然后按照题目的方式建边。最后判断每一对夫妇中丈夫去和妻子去这两个状态是否在同一个强联通分量中,如果在,则为NO,没有则为YES。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1005;

int n, m, en, sccCnt = 0;
int front[N * 2], stk[N * 2], ins[N * 2], low[N * 2], dfn[N * 2];
int color[N * 2];
int di, si;

struct Edge {
    
    
	int v, next;
}e[N * N * 2];

void addEdge(int u, int v) {
    
    
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan(int x) {
    
    
	dfn[x] = low[x] = ++di;
	stk[++si] = x; ins[x] = 1;
	for (int i = front[x]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (!dfn[v]) {
    
    
			tarjan(v);
			low[x] = min(low[x], low[v]);
		}
		else if (ins[v]) {
    
    
			low[x] = min(low[x], dfn[v]);
		}
	}
	if (dfn[x] == low[x]) {
    
    
		int y;
		while (si) {
    
    
			y = stk[si--];
			color[y] = x;
			ins[y] = 0;
			if (x == y) break;
		}
	}
}

void main2() {
    
    
	cin >> m;
	en = di = si = sccCnt = 0;
	for (int i = 1; i <= n * 2; ++i) {
    
    
		low[i] = dfn[i] = front[i] = color[i] = ins[i] = 0;
	}
	for (int i = 1; i <= m; ++i) {
    
    
		int x, y, tx, ty;
		cin >> x >> y >> tx >> ty;
		++x; ++y;
		addEdge(x + n * tx, y + n * (!ty));
		addEdge(y + n * ty, x + n * (!tx));
	}
	for (int i = 1; i <= n * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		if (color[i] == color[i + n]) {
    
    
			cout << "NO\n";
			return;
		}
	}
	cout << "YES\n";
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0); 
	LL _ = 1;
//	cin >> _;
	while (cin >> n) main2();
	return 0;
}

HDU1824 - Let’s go home

题目链接

和前两道题基本没什么不同,按照题目的意思建边后剩下的就和前面的题一模一样。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 10005;

int t, n, m, en;
int front[N * 2], stk[N * 2], ins[N * 2], low[N * 2], dfn[N * 2];
int color[N * 2];
int di, si;

struct Edge {
    
    
	int v, next;
}e[100005];

void addEdge(int u, int v) {
    
    
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan(int x) {
    
    
	dfn[x] = low[x] = ++di;
	stk[++si] = x; ins[x] = 1;
	for (int i = front[x]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (!dfn[v]) {
    
    
			tarjan(v);
			low[x] = min(low[x], low[v]);
		}
		else if (ins[v]) {
    
    
			low[x] = min(low[x], dfn[v]);
		}
	}
	if (dfn[x] == low[x]) {
    
    
		int y;
		while (si) {
    
    
			y = stk[si--];
			color[y] = x;
			ins[y] = 0;
			if (x == y) break;
		}
	}
}

void main2() {
    
    
	cin >> m;
	en = di = si = 0;
	n = t * 3;
	for (int i = 1; i <= n * 2; ++i) {
    
    
		low[i] = dfn[i] = front[i] = color[i] = ins[i] = 0;
	}
	for (int i = 1; i <= t; ++i) {
    
    
		int x, y, z;
		cin >> x >> y >> z;
		++x; ++y; ++z;
		addEdge(x + n, y);
		addEdge(x + n, z);
		addEdge(y + n, x);
		addEdge(z + n, x);
	}
	for (int i = 1; i <= m; ++i) {
    
    
		int x, y;
		cin >> x >> y;
		++x; ++y;
		addEdge(x, y + n);
		addEdge(y, x + n);
	}
	for (int i = 1; i <= n * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		if (color[i] == color[i + n]) {
    
    
			cout << "no\n";
			return;
		}
	}
	cout << "yes\n";
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
//	cin >> _;
	while (cin >> t) main2();
	return 0;
}

HDU3622 - Bomb Game

题目链接

求最小值最大,显然二分。

问题在于我们如何check。我们对于每一个可能的分数,我们来看是否存在这样的选择方式满足。对于一个给定的最小半径,每两个点之间是否矛盾,就在于这两个点之间的距离是否小于我们选择的半径的二倍。按照这层矛盾关系建边,对于每一个二分选出的最小半径 x x x,建立一个图,跑一次2-SAT判断是否可行。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 105;

int n, en;
int x[N * 2], y[N * 2];
int front[N * 2], stk[N * 2], ins[N * 2], low[N * 2], dfn[N * 2];
int color[N * 2];
int di, si;

struct Edge {
    
    
	int v, next;
}e[1000005];

void addEdge(int u, int v) {
    
    
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan(int x) {
    
    
	dfn[x] = low[x] = ++di;
	stk[++si] = x; ins[x] = 1;
	for (int i = front[x]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (!dfn[v]) {
    
    
			tarjan(v);
			low[x] = min(low[x], low[v]);
		}
		else if (ins[v]) {
    
    
			low[x] = min(low[x], dfn[v]);
		}
	}
	if (dfn[x] == low[x]) {
    
    
		int y;
		while (si) {
    
    
			y = stk[si--];
			color[y] = x;
			ins[y] = 0;
			if (x == y) break;
		}
	}
}

double dis(int i, int j) {
    
    
	return sqrt((double)(x[i] - x[j]) * (x[i] - x[j]) + (double)(y[i] - y[j]) * (y[i] - y[j])); 
}

int rev(int x) {
    
    
	if (x <= n) return x + n;
	else return x - n;
}

bool check(double x) {
    
    
	for (int i = 1; i <= n * 2; ++i) {
    
    
		low[i] = dfn[i] = front[i] = color[i] = ins[i] = 0;
	}
	en = di = si = 0;
	for (int i = 1; i <= n * 2; ++i) {
    
    
		for (int j = i + 1; j <= n * 2; ++j) {
    
    
			if (abs(j - i) == n) {
    
    
				continue;
			}
			if (dis(i, j) < x * 2.0) {
    
    
				addEdge(i, rev(j));
				addEdge(j, rev(i));
			}
		}
	}
	for (int i = 1; i <= n * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		if (color[i] == color[i + n]) return false;
	}
	return true;
}

void main2() {
    
    
	for (int i = 1; i <= n; ++i) {
    
    
		cin >> x[i] >> y[i] >> x[i + n] >> y[i + n];
	}
	double l = 0.0, r = 1e6, ans = 0.0;
	while (r - l >= 1e-5) {
    
    
		double mid = (l + r) / 2.0;
		if (check(mid)) {
    
    
			ans = max(ans, mid);
			l = mid;
		}
		else r = mid;
	}
	cout << fixed << setprecision(2) << ans << '\n';
}

int main() {
    
    
//	freopen("Fin.in", "r", stdin);
//	freopen("Fout.out", "w", stdout);
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
//	cin >> _;
	while (cin >> n) main2();
	return 0;
}

LuoguP4171 - [JSOI2010] 满汉全席

题目链接

同一个材料有 2 2 2种结局:被做成了满菜、被做成了汉菜。

在这一点上的实现有两种方法:一种是下面代码中实现的方法,将第 i i i号材料做成满菜编号为 i i i,做成了汉菜编号为 i + n i+n i+n,没有做成满菜编号为 i + 2 n i+2n i+2n,没有做成汉菜编号为 i + 3 n i+3n i+3n

但其实,没有做成满菜就只可能做成汉菜,反过来同理。所以也可以将第 i i i号材料做成满菜编号为 i i i,做成汉菜编号为 i + n i+n i+n。这样是更简洁的编号方式。

关于连边的关系要从材料和评委身上考虑:

从材料上考虑:如果一个材料被做成了满菜,那么就一定不会被做成汉菜;如果一个材料被做成了汉菜,那么就一定不会被做成满菜。

从评委上考虑:假设评委想吃 x , y x,y x,y两种菜,那么如果没做 x x x,就必须做 y y y;如果没做 y y y,就必须做成 x x x

连完边之后进行锁点,最后判断输出即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 205;

int n, m, en;
int front[N * 2], stk[N * 2], ins[N * 2], low[N * 2], dfn[N * 2];
int color[N * 2];
int di, si;

struct Edge {
    
    
	int v, next;
}e[N * 20];

void addEdge(int u, int v) {
    
    
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan(int x) {
    
    
	dfn[x] = low[x] = ++di;
	stk[++si] = x; ins[x] = 1;
	for (int i = front[x]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (!dfn[v]) {
    
    
			tarjan(v);
			low[x] = min(low[x], low[v]);
		}
		else if (ins[v]) {
    
    
			low[x] = min(low[x], dfn[v]);
		}
	}
	if (dfn[x] == low[x]) {
    
    
		int y;
		while (si) {
    
    
			y = stk[si--];
			color[y] = x;
			ins[y] = 0;
			if (x == y) break;
		}
	}
}

int id(string &x) {
    
    
	int ret = 0;
	for (int i = 1; i < x.length(); ++i) {
    
    
		int dig = x[i] - '0';
		ret *= 10;
		ret += dig;
	}
	if (x[0] == 'h') ret += (n >> 1); 
	return ret;
} 

int re(int x) {
    
    
	if (x > n / 2) {
    
    
		return x -= n / 2;
	}
	else return x + n / 2;
}

void main2() {
    
    
	cin >> n >> m;
	en = di = si = 0;
	n <<= 1;
	for (int i = 1; i <= n * 2; ++i) {
    
    
		low[i] = dfn[i] = front[i] = color[i] = ins[i] = 0;
	}
	for (int i = 1; i <= m; ++i) {
    
    
		string x, y;
		cin >> x >> y;
		int xid = id(x), yid = id(y); 
		addEdge(xid + n, yid);
		addEdge(yid + n, xid);
	}
	for (int i = 1; i <= n / 2; ++i) {
    
    
		addEdge(i, re(i) + n);
		addEdge(re(i), i + n);
	}
	for (int i = 1; i <= n * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		if (color[i] == color[i + n]) {
    
    
			cout << "BAD\n";
			return; 
		}
	} 
	for (int i = 1; i <= n / 2; ++i) {
    
    
		if (color[i] == color[i + n / 2]) {
    
    
			cout << "BAD\n";
			return;
		}
	} 
	for (int i = n + 1; i <= n + n / 2; ++i) {
    
    
		if (color[i] == color[i + n / 2]) {
    
    
			cout << "BAD\n";
			return;
		}
	}
	cout << "GOOD\n";
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
	cin >> _;
	while (_--) main2();
	return 0;
}

CF1697F - Too Many Constraints

题目链接

这道题目乍一眼一看没什么思路,因为贪心和DP好像都不太可行。

但是这道题目的形式,无论从题目名称还是题目内容,都是在“限制”二字上做文章。而通过各种限制寻求一个可行解的算法,像极了2-SAT。所以来看一看2-SAT是否可做。

2-SAT的特点是对于每一个变量只有两种取值。我们现在有 n n n个数,每一个数有 k k k个取值,这和2-SAT本身是矛盾的。所以我们要改变一下,将每一个变量定义为每一个位置是否选取 x ( 1 ≤ x ≤ k ) x(1\leq x\leq k) x(1xk)。我们下面用 ( i , x ) (i,x) (i,x)表示 a i = x a_i=x ai=x,用 ! ( i , x ) !(i,x) !(i,x)表示 a i ≠ x a_i\neq x ai=x

在这种定义之下,我们考虑建图。建图需要一些变量之间的关系。在这道题目中,规定我们要构造的序列必须是一个不降的序列,所以对于每一个 ( i , x ) (i,x) (i,x),如果选择了 ( i , x ) (i,x) (i,x),那么对于所有的 c ( i ≤ c ≤ n ) c(i\leq c\leq n) c(icn),就不能选择所有的 ( c , j ) ( 1 ≤ j < x ) (c,j)(1\leq j < x) (c,j)(1j<x);对于所有的 c ( 1 ≤ c < i ) c(1\leq c<i) c(1c<i),就不能选择所有的 ( c , j ) ( x < j ≤ k ) (c,j)(x<j\leq k) (c,j)(x<jk)

然而这样的连边方式会导致图的边数非常庞大,因为对于每一个位置,都要连出 n k nk nk规模的边,那么 n n n个位置就要连 n 2 k n^2k n2k规模的边。这显然是难以实现的。

但是我们可以对于2-SAT换一个定义变量的方式:如果我们定义每一个变量为 ( i , x ) (i,x) (i,x),表示 a i ≥ x a_i\geq x aix,用 ! ( i , x ) !(i,x) !(i,x)表示 a i < x a_i<x ai<x。利用这个定义尝试重新建图。当我们从赋值变成范围之后,整个建图就变得非常简洁了。

这种定义下,我们可以通过 i i i x x x来对 ( i , x ) (i,x) (i,x)进行一个编号。由于每一个位置至多有 k k k个取值,所以我们可以令 ( i , x ) (i,x) (i,x)的编号为 ( i − 1 ) k + x (i-1)k+x (i1)k+x。这样可以选择的情况共有 n k nk nk种。根据2-SAT,我们还要对不选择的情况进行编号。所以 ! ( i , x ) !(i,x) !(i,x)的编号是 ( i − 1 ) k + x + n k (i-1)k+x+nk (i1)k+x+nk。(当然也可以采取别的编号方式)

我们设 i d ( i , x ) id(i,x) id(i,x)表示 ( i , x ) (i,x) (i,x)的编号,那么 i d ( i , x ) id(i,x) id(i,x)的求法是:

int id(int i, int x) {
    
    
	if (i < 1 or i > n or x < 1 or x > k) return -1e9;
	else return (i - 1) * k + x;
}

因为可能会在后面询问一些不存在的点,为了保证到时候给建边的函数一个“这个是不合法情况”的信号,所以在不存在的点进行查询的时候,返回一个很小的值,这样建边的时候,只要传来的编号是负数,就直接返回,不建边。

建边函数addEdge如下:

struct Edge {
    
    
	int v, next;
}e[N * 22];

void addEdge(int u, int v) {
    
    
	if (u <= 0 or v <= 0) {
    
    
		return;
	}
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

从构造不降序列的角度来看:如果 a i ≥ x a_i\geq x aix,那么必然有 a i + 1 ≥ x a_{i+1}\geq x ai+1x;必然有 a i ≥ x − 1 a_i\geq x-1 aix1。同样,如果 a i + 1 < x a_{i+1}<x ai+1<x,那么必然有 a i < x a_i<x ai<x。如果 a x < x − 1 a_x<x-1 ax<x1,则必然有 a x < x a_x<x ax<x

根据这些关系,先进行一个初始的限制的建图,代码如下。

for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= k; ++j) {
    
    
			addEdge(id(i, j), id(i + 1, j));
			addEdge(id(i, j), id(i, j - 1));
			addEdge(id(i, j - 1) + n * k, id(i, j) + n * k);
			addEdge(id(i + 1, j) + n * k, id(i, j) + n * k);
		}
	}

有一点特殊的是,如果 a 1 < 1 a_1<1 a1<1,那么 a 1 ≥ 1 a_1\geq 1 a11必须要成立。这个是必须满足的条件,所以一旦有条件指向了 a 1 < 1 a_1<1 a1<1,这样建边后,会令其恢复成 a 1 ≥ 1 a_1\geq 1 a11的状态,避免出错。

接下来考虑给定的 3 3 3种限制条件该如何转化成我们建边的形式。
(为了写代码的时候避开循环变量 i , j i,j i,j,所以这里取的字母跟题面不同)

对于第一种限制条件: a x ≠ d a_x\neq d ax=d,我们可以把这个语义转换为如果 a x ≥ d a_x\geq d axd,那么 a x ≥ d + 1 a_x\geq d+1 axd+1必然成立;如果 a x < d + 1 a_x<d+1 ax<d+1成立,那么 a x < d a_x<d ax<d也必然成立。把这个语义转化为我们的状态,那就是: ( x , d ) → ( x , d + 1 ) (x,d)\rightarrow (x,d+1) (x,d)(x,d+1) ! ( x , d + 1 ) → ! ( x , d ) !(x,d+1)\rightarrow !(x,d) !(x,d+1)!(x,d)
这里要注意,如果 d = k d=k d=k,那么转换的语义就将是 a x < k a_x<k ax<k

对于第二种限制条件: a x + a y ≤ d a_x+a_y\leq d ax+ayd。我们设 a x ≥ j a_x\geq j axj,那么有 j ≤ a i ≤ d − a y j\leq a_i\leq d-a_y jaiday,从而推出 a y ≤ d − j a_y\leq d-j aydj,即 a y < d − j + 1 a_y<d-j+1 ay<dj+1。同理互换 x , y x,y x,y可以得到如果 a y ≥ j a_y\geq j ayj,那么 a x < d − j + 1 a_x<d-j+1 ax<dj+1
反过来说,如果 a y ≥ d − j + 1 a_y\geq d-j+1 aydj+1,那么 a x < d a_x<d ax<d也必然成立。互换 x , y x,y x,y同理。
将语义转化为状态,可以得到: ( x , j ) → ! ( y , d − j + 1 ) (x,j)\rightarrow !(y,d-j+1) (x,j)!(y,dj+1) ( y , d − j + 1 ) → ! ( x , j ) (y,d-j+1)\rightarrow !(x,j) (y,dj+1)!(x,j) ( y , j ) → ! ( x , d − j + 1 ) (y,j)\rightarrow !(x,d-j+1) (y,j)!(x,dj+1) ( x , d − j + 1 ) → ! ( y , j ) (x,d-j+1)\rightarrow !(y,j) (x,dj+1)!(y,j)
这里要注意的是,如果 d ≤ k d\leq k dk,那么可能会存在 a x = d , a y = 0 a_x=d,a_y=0 ax=d,ay=0 a x = 0 , a y = d a_x=0,a_y=d ax=0,ay=d的情况。为了避免这种情况发生,所以我们要求,如果 a x ≥ d a_x\geq d axd,那么 a x < d a_x<d ax<d;如果 a y ≥ d a_y\geq d ayd,那么 a y < d a_y<d ay<d
转换语义后得到: ( x , d ) → ! ( x , d ) (x,d)\rightarrow !(x,d) (x,d)!(x,d) ( y , d ) → ! ( y , d ) (y,d)\rightarrow !(y,d) (y,d)!(y,d)

对于第三种限制条件: a x + a y ≥ d a_x+a_y\geq d ax+ayd。我们社 a x < j a_x<j ax<j,那么有 j > a x ≥ d − a y j>a_x\geq d-a_y j>axday,从而推出 a y > d − j a_y>d-j ay>dj,即 a y ≥ d − j + 1 a_y\geq d-j+1 aydj+1。同理互换 x , y x,y x,y可以得到如果 a y < j a_y<j ay<j,那么 a x ≥ d − j + 1 a_x\geq d-j+1 axdj+1
反过来说,如果 a y < d − j + 1 a_y<d-j+1 ay<dj+1,那么 a x ≥ j a_x\geq j axj也必然成立。互换 x , y x,y x,y同理。
将语义转化为状态,可以得到 ! ( x , j ) → ( y , d − j + 1 ) !(x,j)\rightarrow (y,d-j+1) !(x,j)(y,dj+1) ( x , j ) → ! ( y , d − j + 1 ) (x,j)\rightarrow !(y,d-j+1) (x,j)!(y,dj+1) ! ( y , j ) → ( x , d − j + 1 ) !(y,j)\rightarrow (x,d-j+1) !(y,j)(x,dj+1) ( y , j ) → ! ( x , d − j + 1 ) (y,j)\rightarrow !(x,d-j+1) (y,j)!(x,dj+1)
这里要注意的是:如果 d > k d>k d>k,为了避免 a x , a y a_x,a_y ax,ay中有一个数大于 k k k,即另外一个数小于 d − k d-k dk,所以我们要求,如果 a x < d − k a_x<d-k ax<dk,那么 a x ≥ d − k a_x\geq d-k axdk;如果 a y < d − k a_y<d-k ay<dk,那么 a y ≥ d − k a_y\geq d-k aydk
转换语义后得到: ! ( x , d − k ) → ( x , d − k ) !(x,d-k)\rightarrow (x,d-k) !(x,dk)(x,dk) ! ( y , d − k ) → ( y , d − k ) !(y,d-k)\rightarrow (y,d-k) !(y,dk)(y,dk)

建边结束后,走一遍tarjan算法锁点,求强连通分量。

对于这 n k nk nk个情况,我们分别看看每一个情况取或者不取是否在同一个强联通分量中,如果在,则直接输出 − 1 -1 1

如果都不在同一个强联通分量中,那么我们的答案一定存在。接下来构造序列。

对于 n n n个位置的每一个位置,我们都尝试从大数向小数选取,如果有一个可行取值,那么就直接填入答案并退出。根据我们对状态的定义,是 a i ≥ x a_i\geq x aix,所以我们从大到小遍历所有可能的取值,如果 a i ≥ d a_i\geq d aid满足而 a i ≥ d + 1 a_i\geq d+1 aid+1不满足,那么 a i = d a_i=d ai=d a i a_i ai可选的最大值。选择最大值的好处是不需要再看其他的取值的同时,不会因为这个位置选择的值小而导致序列不再满足不减性质。

具体判断这个数是否可行,只需要判断第 i i i个位置取 j j j这个数的情况中,选取 ( i , j ) (i,j) (i,j)的状态所在的强联通分量编号和 ! ( i , j ) !(i,j) !(i,j)的状态所在的强联通分量的大小关系,如果 ( i , j ) (i,j) (i,j)更小,那么就选择 ( i , j ) (i,j) (i,j)(可以认为 ( i , j ) (i,j) (i,j)的值为真,就等同于洛谷的那道模板题的输出解的做法)。

最后输出数组即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;

int n, m, k, en;
int front[N * 22], stk[N * 22], ins[N * 22], low[N * 22], dfn[N * 22];
int color[N * 22], ans[N * 2];
int di, si, colcnt;

struct Edge {
    
    
	int v, next;
}e[N * 22];

void addEdge(int u, int v) {
    
    
	if (u <= 0 or v <= 0) {
    
    
		return;
	}
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan (int u) {
    
    
	dfn[u] = low[u] = ++di;
	stk[++si] = u; ins[u] = 1;
	for (int i = front[u]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (dfn[v] == 0) {
    
    
			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]) {
    
    
		colcnt++;
		while (true) {
    
    
			int v = stk[si--]; ins[v] = 0;
			color[v] = colcnt;
			if (u == v) break;
		}
	}
}

int id(int i, int x) {
    
    
	if (i < 1 or i > n or x < 1 or x > k) return -1e9;
	else return (i - 1) * k + x;
}

void main2() {
    
    
	cin >> n >> m >> k;
	en = di = si = colcnt = 0;
	for (int i = 1; i <= n * k * 2; ++i) {
    
    
		low[i] = dfn[i] = front[i] = color[i] = ins[i] = 0;
	}
	for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= k; ++j) {
    
    
			addEdge(id(i, j), id(i + 1, j));
			addEdge(id(i, j), id(i, j - 1));
			addEdge(id(i, j - 1) + n * k, id(i, j) + n * k);
			addEdge(id(i + 1, j) + n * k, id(i, j) + n * k);
		}
	}
	addEdge(id(1, 1) + n * k, id(1, 1));
	for (int i = 1; i <= m; ++i) {
    
    
		int o, x, y, d;
		cin >> o;
		if (o == 1) {
    
    
			cin >> x >> d;
			if (d == k) addEdge(id(x, d), id(x, d) + n * k);
			else {
    
    
				addEdge(id(x, d), id(x, d + 1));
				addEdge(id(x, d + 1) + n * k, id(x, d) + n * k);
			}
		}
		else if (o == 2) {
    
    
			cin >> x >> y >> d;
			if (d <= k) {
    
    
				addEdge(id(x, d), id(x, d) + n * k);
				addEdge(id(y, d), id(y, d) + n * k);
			}
			for (int j = 1; j <= k; ++j) {
    
    
				addEdge(id(x, j), id(y, d - j + 1) + n * k);
				addEdge(id(y, d - j + 1), id(x, j) + n * k);
				addEdge(id(y, j), id(x, d - j + 1) + n * k);
				addEdge(id(x, d - j + 1), id(y, j) + n * k);
			}
		}
		else {
    
    
			cin >> x >> y >> d;
			if (d > k) {
    
    
				addEdge(id(x, d - k) + n * k, id(x, d - k));
				addEdge(id(y, d - k) + n * k, id(y, d - k));
			}
			for (int j = 1; j <= k; ++j) {
    
    
				addEdge(id(x, j) + n * k, id(y, d - j + 1));
				addEdge(id(y, d - j + 1) + n * k, id(x, j));
				addEdge(id(y, j) + n * k, id(x, d - j + 1));
				addEdge(id(x, d - j + 1) + n * k, id(y, j));
			}
		}
	}
	for (int i = 1; i <= n * k * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= k; ++j) {
    
    
			if (color[id(i, j)] == color[id(i, j) + n * k]) {
    
    
				cout << -1 << '\n';
				return;
			}
		}
		for (int j = k; j >= 1; --j) {
    
    
			if (color[id(i, j)] < color[id(i, j) + n * k]) {
    
    
				ans[i] = j; 
				break;
			}
		}
	}
	for (int i = 1; i <= n; ++i) {
    
    
		cout << ans[i] << ' ';
	}
	cout << '\n';
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
	cin >> _;
	while (_--) main2();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/xhyu61/article/details/126287028