P9869 [NOIP2023] 三值逻辑 题解

题意

  • 一种数据类型 tribool,有三种取值 T、F、U。给出一系列赋值操作,求出使得“赋值后的值与初值相等”的最少的“初值为 U 的变量个数”。

分析

  • 首先,所有直接赋值成 U 的初值一定为 U;因为 T 和 F 之间的运算与 bool 相同,所以如果赋值为 T 或 F 出现矛盾的话也只能赋初值为 U。
  • \(fa_x\) 来表示变量 \(x\) 的赋值来源是谁,\(sgn_x\) 表示是否取反。也就是说,变量 \(x\) 的末值就是变量 \(fa_x\) 的初值经过 \(sgn_x\) 运算后的值。可以发现,由于 \(fa_x\) 的初值就是 \(fa_x\) 的末值,所以可以让 \(fa_x\) 先确定初(末)值,然后再直接赋给 \(x\)
  • 把这个过程抽象成图,建立从 \(fa_x\) 指向 \(x\) 的边,边权为 \(sgn_x\),然后拓扑排序赋值即可。
  • 可以发现如果从一个非环内的点出发,一定不会到达一个环,因为每个点的入度最多为 \(1\)
  • 对于环,可以发现如果环内 \(sgn_x=-1\) 的个数为奇数的话就是非法的,需要赋值为 U,否则赋为 T 或者 F 都可。挑一个环内的点赋值后将该点放入队列中(拆环)就可以继续拓排赋值。我使用的是 Floyd 判环。
  • 一个避免特判的小技巧是把 T 设为第 \(n+1\) 个变量,U 设为第 \(n+2\) 个变量,统计答案时将第 \(n+2\) 个点排除即可。

考场代码

#include <bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n, m, fa[N], sgn[N], num[N], du[N], ans;
int head[N], to[N], nxt[N], sg[N], tot;
bool vis[N];
queue<int> q;

inline void read(int &x) {
	char ch = x = 0;
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9') {
		x = (x << 1) + (x << 3) + ch - 48;
		ch = getchar();
	}
	return ;
}

inline void print(int x) {
	static int stk[50];
	int top = 0;
	do {
		stk[top++] = x % 10;
		x /= 10;
	} while (x);
	while (top) {
		putchar(stk[--top] + 48);
	}
	putchar('\n');
	return ;
}

inline void add(int u, int v, int w) {
	to[tot] = v;
	sg[tot] = w;
	nxt[tot] = head[u];
	head[u] = tot++;
	return ;
}

inline void color() { //拓扑排序,想不到名字就叫上色了
	int x;
	while (!q.empty()) {
		x = q.front();
		q.pop();
		if (vis[x]) continue;
		vis[x] = 1;
		if (num[x] == 2 && x <= n) ans++; //统计答案,排除源点 U
		if (num[x] == 0) { //赋初值
			num[x] = 1;
		}
		for (int i = head[x]; ~i; i = nxt[i]) {
			if (num[x] == 2) { //也是赋初值
				num[to[i]] = 2;
			} else {
				num[to[i]] = num[x] * sg[i]; //其实这里乘不乘都无所谓,不过我还是模拟了
			}
			du[to[i]]--;
			if (du[to[i]] == 0) q.push(to[i]);
		}
	}
	return ;
}

void work(int x) {
	int s = fa[fa[x]], t = fa[x], cnt = 0;
	while (s != t) { //Floyd 判环
		s = fa[fa[s]];
		t = fa[t];
	}
	do { //统计取反个数
		cnt += (sgn[s] == 1 ? 0 : 1);
		s = fa[s];
	} while (s != t);
	if (cnt & 1) { //赋值拆环
		num[s] = 2;
	} else {
		num[s] = 1;
	}
	du[s]--;
	q.push(s);
	color(); //拆了环后再次拓排
	return ;
}

signed main() {
//	freopen("tribool.in", "r", stdin);
//	freopen("tribool.out", "w", stdout);
	int c, T, x, y;
	char ch;
	read(c), read(T);
	while (T--) {
		read(n), read(m);
		tot = ans = 0;
		memset(num, 0, sizeof num);
		memset(du, 0, sizeof du);
		memset(vis, 0, sizeof vis);
		memset(head, -1, sizeof head);
		num[n + 1] = 1;
		num[n + 2] = 2; //初始化两个源点
		while (!q.empty()) q.pop();
		for (int i = 1; i <= n; i++) {
			fa[i] = i;
			sgn[i] = 1; //每个值最初都指向自己,符号为正
		}
		for (int i = 1; i <= m; i++) {
			ch = getchar();
			while (ch != 'T' && ch != 'F' && ch != 'U' && ch != '+' && ch != '-') ch = getchar();
			read(x);
			if (ch == 'T') { //各种赋值操作
				fa[x] = n + 1;
				sgn[x] = 1;
			} else if (ch == 'F') {
				fa[x] = n + 1;
				sgn[x] = -1;
			} else if (ch == 'U') {
				fa[x] = n + 2;
			} else if (ch == '+') {
				read(y);
				fa[x] = fa[y]; //这里的赋值要指向 fa[y] 而不是 y,下同
				sgn[x] = sgn[y];
			} else {
				read(y);
				fa[x] = fa[y];
				sgn[x] = -sgn[y];
			}
		}
		for (int i = 1; i <= n; i++) { //建图
			add(fa[i], i, sgn[i]);
			du[i]++;
		}
		for (int i = 1; i <= n + 2; i++) { //拓排初始化
			if (du[i] == 0) {
				q.push(i);
			}
		}
		color(); //首次拓排
		for (int i = 1; i <= n; i++) {
			if (!vis[i]) { //处理环的情况
				work(i);
			}
		}
		print(ans);
	}
	return 0;
}

感谢观看~有错请大佬们在评论区指出

posted @ 2024-02-27 19:46  wswwhcs  阅读(109)  评论(0编辑  收藏  举报