当前位置: 首页 > news >正文

2-SAT 学习报告

原来是没学习报告这东西的,但是由于 2-SAT 感觉太过邪门,就写一下(毕竟我擅长的不是图论,数据结构什么的可能就写的不多)。

介绍:

由于 k-SAT(\(k>2\)) 是什么所谓的 NP 完全问题,导致只能打暴力,所以一般都是 2-SAT 问题,一般会有几个限制条件:a x b y 表示 \(a\)\(x\)\(b\)\(y\) 时这个条件才为真,所有条件都为真才有答案。经过观察我们发现,当 \(a\)\(!x\) 时, \(b\) 必须要为 \(y\),同理,\(b\)\(!y\) 时,\(a\) 必须要为 \(x\)。我们发现这个很像建图,于是我们把一个点拆成两个,分别代表这个点取 1 或 0。忘了说了,\(x\)\(y\) 要么是 0 要么是 1。然后按照我们观察到的关系连边,每组关系连两条边,那么我们还要根据 \(x\)\(y\) 的不同来分类讨论,非常麻烦,但这有个快捷的:

add(a + (x & 1)*n, b + (y ^ 1)*n);
add(b + (y & 1)*n, a + (x ^ 1)*n);

我们设 \(i\) 为真,\(i+n\) 为假,那么可以看出这个就是取与条件值相反的值的点连向值必须与条件值相同的点。举个栗子,加边第一个要为假,如果 \(x\) 为 0,那么相当于 \(!a\),那么我们让 \(a\) 的值为真,那么加在一起就是假。而 \(i_a\) 为真,\(i_a+n\) 为假,所以上述柿子前一串的值就是 \(i_a\)

然后怎么做呢,我们发现,当你从一个点可以通过单向边到达同一个元素拆出来的另一个点,这个点就不能作为取值,当另一个点也可以到达这个点时,那么答案无解(两个点都不能取了)。我们发现,无解的情况就是两个点都在同一个强连通分量里面,那么我们使用 tarjan 缩点,缩完后先循环枚举判断是否无解。
那么如果有解呢,我们发现,通向另一个点那个无法作为答案的点在拓扑排序的时间戳是更小的,那么我们选择时间戳更大的不就行了。所以我们缩完点在跑一遍拓扑,然后……好麻烦。

但是我们想到,tarjan 不也有时间戳吗,但是好像没法用……那么所在强连通分量的顺序呢?我们知道,每个点都会在一个强连通分量里面,那么强连通分量的顺序不也可以当成拓扑的顺序吗?然后我们发现,一个棵深度越深,强连通分量标号越小,也就是说这是和拓扑反着的,原来我们要选拓扑序更大的,现在只用选时间戳编号更小的就行了。

将军说:数组一定要开两倍空间!lol lol lol

给出模板题代码:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 2000005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int a, b, n, m, x, y, bcc, top;
int dfn[N], low[N], num[N], vis[N], stack[N];void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1;stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}int main()
{n = read(), m = read();for (rnt i = 1; i <= m; i++){a = read(), x = read(), b = read(), y = read();add(a + (x & 1)*n, b + (y ^ 1)*n);add(b + (y & 1)*n, a + (x ^ 1)*n);}cnt = 0;for (rnt i = 1; i <= 2 * n; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= n; i++)if (num[i] == num[i + n])return puts("IMPOSSIBLE"), 0;puts("POSSIBLE");for (rnt i = 1; i <= n; i++)printf("%d ", num[i] <= num[i + n]);return 0;
}

顺带一提,由于圆方树写多了且忘了点双联通分量和强连通分量的区别,导致这里编号仍然用的 bcc

例题:

一:POJ 3683 Priest John's Busiest Day

一道练手的好题,但是要开很大的数组(不然你可以发现大量的 RE),讲一下具体细节:

我们发现仪式只有两个举办时间:开始和结尾,我们只能选择其中一个,而每个婚礼都要举办一次仪式。我们发现这简直就是 2-SAT 问题。那么我们先把点拆成两份,由于只要时间有冲突就要建边,所以我们循环枚举(毕竟 \(n\) 只有 1000),而这个枚举会建出大量的边,数组要开很大。另外神父前往下一个仪式的速度堪比他去找小男孩,所以两场仪式的开始和结尾的时间是可以重叠的。另外可能婚礼办完都没举办完仪式,所以还要特判婚礼时间是否小于仪式举办时间。

剩下的就是判断是否有解和输出了,我们设开始举办仪式的点编号为 \(i\),结尾举办的点为 \(i+n\)。那么取所在强连通分量小的输出答案,输出方式有点反人类。
代码如下:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 2000005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int n, m, a, b, d, u, v, x, y, hh1, mm1, hh2, mm2, bcc, top;
int dfn[N], low[N], num[N], vis[N], stack[N];struct node
{int l, r, id, z;
} s[N];void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1, stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}bool check(int a, int b)
{if (s[a].l >= s[b].r)//可以重叠return 0;if (s[a].r <= s[b].l)return 0;return 1;
}int main()
{bool f = 1;n = read();for (rnt i = 1; i <= n; i++){scanf("%d:%d", &hh1, &mm1), scanf("%d:%d", &hh2, &mm2), d = read();a = hh1 * 60 + mm1, b = hh2 * 60 + mm2;if  (b - a < d)f = 0;s[++cnt] = {a, a + d, i, 0}, s[++cnt] = {b - d, b, i, 1};}if (!f)return puts("NO"), 0;cnt = 0;for (rnt i = 1; i <= 2 * n; i++){a = s[i].id, x = s[i].z;for (rnt j = 1; j < i; j++)if (check(i, j) && s[i].id != s[j].id){b = s[j].id, y = s[j].z;add(a + (x & 1)*n, b + (y ^ 1)*n);add(b + (y & 1)*n, a + (x ^ 1)*n);}}for (rnt i = 1; i <= 2 * n; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= n; i++)if (num[i] == num[i + n])return puts("NO"), 0;puts("YES");for (rnt i = 1; i <= n; i++)if (num[i] < num[i + n]){a = s[2 * i - 1].l, b = s[2 * i - 1].r;x = a / 60;if (x < 10)write(0);write(x), pr(58);x = a % 60;if (x < 10)write(0);write(x), pr(32);x = b / 60;if (x < 10)write(0);write(x), pr(58);x = b % 60;if (x < 10)write(0);write(x), pr(10);}else{a = s[2 * i].l, b = s[2 * i].r;x = a / 60;if (x < 10)write(0);write(x), pr(58);x = a % 60;if (x < 10)write(0);write(x), pr(32);x = b / 60;if (x < 10)write(0);write(x), pr(58);x = b % 60;if (x < 10)write(0);write(x), pr(10);}return 0;
}

二:HDU 3622 Bomb Game

二分和 2-SAT 的结合体,毕竟都是 2 嘛,首先记得是多测,其次每次二分都要清空那些重要的数组(指没改会影响答案的),然后注意浮点数判等会把你坑死,创一个 eps=1e-7 的变量来判等,然后记得有欧几里得距离判断两圆是否重叠(欧几里得距离小于等于两倍半径)。
代码:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 2000005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int n, m, a, b, x, y, bcc, top;
int dfn[N], low[N], num[N], vis[N], stack[N];struct node
{int x, y, id, z;
} s[N];void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1, stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}bool sat(double r)
{for (rnt i = 2; i <= 2 * n; i++)for (rnt j = 1; j < i; j++)if (s[i].id != s[j].id && sqrt((s[i].x - s[j].x) * 1.0 * (s[i].x - s[j].x) + (s[i].y - s[j].y) * 1.0 * (s[i].y - s[j].y)) <= r * 2){a = s[i].id, x = s[i].z, b = s[j].id, y = s[j].z;add(a + (x & 1)*n, b + (y ^ 1)*n);add(b + (y & 1)*n, a + (x ^ 1)*n);}for (rnt i = 1; i <= 2 * n; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= n; i++)if (num[i] == num[i + n])return 0;return 1;
}int main()
{while (cin >> n){for (rnt i = 1; i <= n; i++){a = read(), b = read(), x = read(), y = read();s[++cnt] = {a, b, i, 0}, s[++cnt] = {x, y, i, 1};}cnt = 0;double l = 0.0, r = 20000.0, ans = 0.0, eps = 1e-7;while (r - l > eps){double mid = (l + r) / 2;for (rnt i = 1; i <= 2 * n; i++)dfn[i] = num[i] = vis[i] = head[i] = stack[i] = 0;bcc = cnt = top = 0;if (sat(mid))ans = mid, l = mid;elser = mid;}printf("%.2lf\n", ans);for (rnt i = 1; i <= 2 * n; i++)dfn[i] = num[i] = vis[i] = head[i] = stack[i] = 0;bcc = cnt = top = 0;}return 0;
}

三:HDU 4115 Eliminate the Conflict

一道好题,难点在于怎样合适地连边,2-SAT 的边 \(a\to b\) 表示选了 \(a\) 就一定要选 \(b\)。但假如像下面这样连边:

if (a1 != b1)add(x, y), add(y, x);
if (a1 != b2)add(x, y + n), add(y + n, x);
if (a2 != b1)add(x + n, y), add(y, x + n);
if (a2 != b2)add(x + n, y + n), add(y + n, x + n);

眼尖的人一眼看出绝对会错,只要连上几条边那么两个同一元素拆出的就会在同一个强连通分量里,而且它不满足选了 \(a\) 就一定选 \(b\),因为 \(a\) 可能连两条边。

那么具体该怎么做呢?我们发现石头剪刀布要求每把都赢或平只有两种选项,我们设为 \(a_1\)\(a_2\),另一个数字的就是 \(b_1\)\(b_2\)。当 \(a_1=b_1\) 时,那么 \(a_1\) 只能连接 \(b_2\),同理,\(b_1\) 只能连接 \(a_2\),当多个矛盾的选项都连上了边,自然会无解。

我们发现,这就符合 2-SAT 的规则:只链接明确的边,另外是连接那些不出矛盾的边,有时不是题干让你连啥就连啥。比如这道题,如果按照题干什么选相同的不同的就会错,有时就要连不矛盾但与题干不同的边(感觉还是有点绕啊)。

代码:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 2000005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int n, m, a1, a2, b1, b2, k, x, y, bcc, top;
int a[N], dfn[N], low[N], num[N], vis[N], stack[N];void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1, stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}int main()
{int time = read();for (rnt ti = 1; ti <= time; ti++){bool f = 1;n = read(), m = read();for (rnt i = 1; i <= n; i++)a[i] = read();for (rnt i = 1; i <= m; i++){x = read(), y = read(), k = read();a1 = a[x], a2 = a[x] == 1 ? 3 : a[x] - 1, b1 = a[y], b2 = a[y] == 1 ? 3 : a[y] - 1;if (k){if (a1 == b1)add(x, y + n), add(y, x + n);if (a1 == b2)add(x, y), add(y + n, x + n);if (a2 == b1)add(x + n, y + n), add(y, x);if (a2 == b2)add(x + n, y), add(y + n, x);}else{if (a1 != b1)add(x, y + n), add(y, x + n);if (a1 != b2)add(x, y), add(y + n, x + n);if (a2 != b1)add(x + n, y + n), add(y, x);if (a2 != b2)add(x + n, y), add(y + n, x);}}cnt = 0;for (rnt i = 1; i <= 2 * n; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= n; i++)if (num[i] == num[i + n])f = 0;cout << "Case #" << ti << ": ";if (f)puts("yes");elseputs("no");for (rnt i = 1; i <= 2 * n; i++)dfn[i] = num[i] = vis[i] = head[i] = stack[i] = 0;bcc = cnt = top = 0;}return 0;
}

四:POJ 3678 Katu Puzzle

一道让我很困惑的题,主要是它的连边思想,我们发现,这道题很适合用 2-SAT 来做,但问题出在下面两处:
AND\(c=1\) 时,我们不需要将 \(a\) 元素表示取 1 的点连向 \(b\) 元素表示取 1 的部分(\(b\)\(a\) 同理)。而是将 \(a+n\) 连向 \(a\)\(b+n\) 连向 \(b\)。这样可以直接排除选 \(a+n\)\(b+n\) 的情况,但我并不清楚为什么要这么做。
OR:同理,当 \(c=0\) 时,要直接连边排除选 \(a\)\(b\) 的情况。

但其实上述的边连上后再连原来要连的也不会出错,所以其实这是一种去掉错误状态的做法?
代码:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 4000005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int n, m, a, b, c, bcc, top;
int dfn[N], low[N], num[N], vis[N], stack[N];
string s;void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1, stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}int main()
{n = read(), m = read();for (rnt i = 1; i <= m; i++){a = read() + 1, b = read() + 1, c = read(), cin >> s;if (s == "AND")if (c)add(a + n, a), add(b + n, b), add(a, b), add(b, a);elseadd(a, b + n), add(b, a + n);if (s == "OR")if (c)add(a + n, b), add(b + n, a);elseadd(a, a + n), add(b, b + n), add(a + n, b + n), add(b + n, a + n);if (s == "XOR")if (c)add(a, b + n), add(b, a + n), add(a + n, b), add(b + n, a);elseadd(a, b), add(b, a), add(a + n, b + n), add(b + n, a + n);}cnt = 0;for (rnt i = 1; i <= 2 * n; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= n; i++)if (num[i] == num[i + n])return puts("NO"), 0;puts("YES");return 0;
}

最后再重新理一下思路,所有的正常加边都好理解,如果只有这种选择就直接连边,多个选择不连。当有怎么连都不行的,那么就让它连向它的兄弟点(就是由同一个元素拆成的点),这样就把它排除了,这很重要。

五:CF776D The Door Problem

虽然是道绿题,但是难度可能要比模版的什么的都要高,但是由于可以并查集加二分图踹飞所以降绿了。
我们发现一个门只对应两个钥匙,就对应两个点,我们发现当一个门原来是打开的,要吗两个钥匙都不用,要吗都用,所以连四条边;当门原来是关着的,那只用一把钥匙,再建四条边。由于一把钥匙用两次不优,所以这样设计状态是正确的。

最后贴上代码:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 400005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int n, m, a, b, k, bcc, top;
int w[N], dfn[N], low[N], num[N], vis[N], stack[N];
int f[N][2];void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1, stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}int main()
{n = read(), m = read();for (rnt i = 1; i <= n; i++)w[i] = read();for (rnt i = 1; i <= m; i++){k = read();for (rnt j = 1; j <= k; j++)a = read(), f[a][0] ? f[a][1] = i : f[a][0] = i;}for (rnt i = 1; i <= n; i++)if (w[i])add(f[i][0], f[i][1]), add(f[i][1], f[i][0]), add(f[i][0] + m, f[i][1] + m), add(f[i][1] + m, f[i][0] + m);elseadd(f[i][0], f[i][1] + m), add(f[i][1] + m, f[i][0]), add(f[i][1], f[i][0] + m), add(f[i][0] + m, f[i][1]);for (rnt i = 1; i <= 2 * m; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= m; i++)if (num[i] == num[i + m])return puts("NO"), 0;puts("YES");return 0;
}

六:Luogu P3825 游戏

曾经好像是一道黑,现在成紫了,难度很大,看了题解都还是晕的。
来看一下题,如果没有万能图,这道题就是基础的 2-SAT,但是有,所以被一拳锤飞了。

首先是问题一,我们怎么除了万能图,这里想到我们可以枚举每个万能图是哪种图(\(abc\) 里的),那么时间复杂的就是 \(O(3^d\cdot(n+m))\) 在这道题会超时,那怎么办,我们(除了我)发现,没必要枚举三个状态,枚举两个就可以包含三种车,所以我们可以二进制状压来枚举。
这可以引用题解的话;

所以在枚举只有B,C两种选择的时候,由于断绝了A对于i的所有联系(如果你不明白可以看看处理A赛车的方式,上述方式使得枚举过程和A变得毫无关系,即绝对不选A),所以我们相当于判断了第i个位置选B是否可行,以及选C是否可行。

综上所述,若枚举“只有A,C两种选择”“只有B,C两种选择”两种情况,我们就遍历了三种情况:选B是否可行,选C是否可行,以及选A是否可行。

然后就是建图,如果第一个点输入的字符刚好是此图不支持的,那就直接 continue,如果第二个点输入的字符也是此图不支持的,那么第一个点输入的字符相当于无法使用,这就用到我们上道题用的方法,让它连接兄弟点,把它排除。否则就正常的建图,别忘了它的所谓的逆否命题。
代码:

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 2000005
#define M 1000000007
using namespace std;inline ll read()
{ll x = 0, f = 1;char ch = gr();while (ch < '0' || ch > '9')ch == '-' ? f = -1, ch = gr() : ch = gr();while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();return x * f;
}inline void write(ll x)
{static int sta[39], top = 0;if (x < 0)pr('-'), x = -x;dosta[++top] = x % 10, x /= 10;while (x);while (top)pr(sta[top--] ^ 48);
}struct edge
{int v, next;
} e[N << 1];
int head[N], cnt;inline void add(int u, int v)
{e[++cnt] = {v, head[u]}, head[u] = cnt;
}
int n, m, a, b, d, bcc, top;
int _a[N], _b[N], x[N], dfn[N], low[N], num[N], vis[N], stack[N];
int k[N][2];
char ch1, ch2;
char _ch1[N], _ch2[N];
string s;void tarjan(int u)
{dfn[u] = low[u] = ++cnt;vis[u] = 1, stack[++top] = u;for (rnt i = head[u]; i; i = e[i].next){int v = e[i].v;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]){bcc++;while (stack[top + 1] != u)num[stack[top]] = bcc, vis[stack[top--]] = 0;}
}int main()
{cin.tie(0);n = read(), d = read();cin >> s;s = " " + s;for (rnt i = 1; i < s.size(); i++)switch (s[i]){case 'x':x[++cnt] = i;break;case 'a':k[i][0] = 'B', k[i][1] = 'C';break;case 'b':k[i][0] = 'A', k[i][1] = 'C';break;case 'c':k[i][0] = 'A', k[i][1] = 'B';break;}m = read();for (rnt i = 1; i <= m; i++)_a[i] = read(), cin >> _ch1[i], _b[i] = read(), cin >> _ch2[i];for (rnt j = 0; j < (1 << d); j++){bool f = 1;for (rnt i = 1; i <= 2 * n; i++)dfn[i] = num[i] = vis[i] = head[i] = stack[i] = 0;bcc = cnt = top = 0;for (rnt i = 1; i <= d; i++)if (j >> (i - 1))k[x[i]][0] = 'B', k[x[i]][1] = 'C';elsek[x[i]][0] = 'A', k[x[i]][1] = 'C';for (rnt i = 1; i <= m; i++){a = _a[i], ch1 = _ch1[i], b = _b[i], ch2 = _ch2[i];if (k[a][0] != ch1 && k[a][1] != ch1)continue;else if (k[b][0] != ch2 && k[b][1] != ch2)if (k[a][0] == ch1)add(a, a + n);elseadd(a + n, a);else if (k[a][0] == ch1)if (k[b][0] == ch2)add(a, b), add(b + n, a + n);elseadd(a, b + n), add(b, a + n);else if (k[b][0] == ch2)add(a + n, b), add(b + n, a);elseadd(a + n, b + n), add(b, a);}for (rnt i = 1; i <= 2 * n; i++)if (!dfn[i])tarjan(i);for (rnt i = 1; i <= n; i++)if (num[i] == num[i + n])f = 0;if (f){for (rnt i = 1; i <= n; i++)if (num[i] < num[i + n])pr(k[i][0]);elsepr(k[i][1]);return 0;}}puts("-1");return 0;
}

七:Luogu P6378 Riddle

八:HDU 1824 Let's go home

九:Luogu 3007 The Continental Cowngress G

十:Luogu P3513 KON-Conspiracy

十一:Luogu P3209 平面图判定

十二:UVA1146 Now or later

十三:AT_arc069_d [ARC069F] Flags

十四:CF587D Duff in Mafia

这个报告差不多写完了……
那么应该……

感谢您的观看!

http://www.aitangshan.cn/news/234.html

相关文章:

  • ces
  • day38
  • CSP-J 模拟1解析
  • 20250811
  • 《Effective C++》(1,2)
  • 数组
  • CSP-S模拟赛11 总结
  • CSP-S模拟赛12 总结
  • 旋转表达:blender下骨骼重映射的公式推导 bone animation retarget
  • 进度
  • 一名OIER的开始
  • springboot监听redisKey过期 - br
  • 你好我好一切都好 - Karry
  • 数据库操作例题
  • 02010901 表达式和运算符
  • 浏览器面试题及详细答案 88道(01-11) - 详解
  • WBLT学习笔记
  • 敏宝
  • 图论
  • 【自学嵌入式:stm32单片机】旋转编码器记次
  • 乌班图静态网址动态网址
  • 用户以及赋权还有备份数据库
  • 立个Flag,重新开始使用cnblog - by
  • 做题日志2025.8
  • 数据库
  • 02010803 类和继承03-静态类、扩展方法、命名约定
  • 我设计的IP地址(3)
  • base44
  • 2025.8.11总结 - A
  • ftp服务详解