371. 牧师约翰最忙碌的一天(tarjan,2-SAT)

371. 牧师约翰最忙碌的一天 - AcWing题库

牧师约翰在 9 月 1 日这天非常的忙碌。

有 N 对情侣在这天准备结婚,每对情侣都预先计划好了婚礼举办的时间,其中第 i 对情侣的婚礼从时刻 Si 开始,到时刻 Ti 结束。

婚礼有一个必须的仪式:站在牧师面前聆听上帝的祝福。

这个仪式要么在婚礼开始时举行,要么在结束时举行。

第 i 对情侣需要 Di 分钟完成这个仪式,即必须选择 Si∼Si+Di 或 Ti−Di∼Ti 两个时间段之一。

牧师想知道他能否满足每场婚礼的要求,即给每对情侣安排Si∼Si+Di 或 Ti−Di∼Ti,使得这些仪式的时间段不重叠。

若能满足,还需要帮牧师求出任意一种具体方案。

注意,约翰不能同时主持两场婚礼,且 所有婚礼的仪式均发生在 9 月 1 日当天

如果一场仪式的结束时间与另一场仪式的开始时间相同,则不算重叠。

例如:一场仪式安排在 08:00∼09:00,另一场仪式安排在 09:00∼10:00,则不认为两场仪式出现重叠。

输入格式

第一行包含整数 N。

接下来 N 行,每行包含 Si,Ti,Di,其中 Si 和 Ti 是 hh:mm 形式。

输出格式

第一行输出能否满足,能则输出 YES,否则输出 NO

接下来 N 行,每行给出一个具体时间段安排。

数据范围

1≤N≤1000

输入样例:
2
08:00 09:00 30
08:15 09:00 20
输出样例:
YES
08:00 08:30
08:40 09:00

解析: 

如果我们将每场婚礼看作是一个变量,那么每个变量就会有 开始时举办仪式 和 结束时举办仪式 两种取值,那么就启发我们可以用 2-SAT 来求解。将两种取值分别记为 x[i][0] 和 x[i][1],并看作节点 i 和 i + N

枚举每两场婚礼 i, j,若婚礼 i 的 x[i][p] 时间段与婚礼 j 的 x[j][q] 时间段重叠,则说明两者不能同时
被选为最终赋值。转化成 2-SAT 的形式,应该连 (i + p * N, j + (1 - q) * N) 和 (j + q * N, i + (1 - p) * N)两条有向边,两者互为逆否命题。

用 Tarjan 算法求强联通分量,检查是否存在 i 和 i + N 在同一个强连通分量即可。本题还需要输出方案。

这里给出 2-SAT 合法方案的两种构造方法。

第一种构造方法

首先,在一个强连通分量中,只要确定了一个变量的赋值,该强连通分量内其他变量的赋值也就直接确定了,
这启发我们考虑缩点,其次,因为互为逆否命题的有向边在图中成对出现,所以一个零出度点对面的点一定有出边。
选择一个有出边的店会使得该边指向的点必须也被选择,而选择一个零出度点则不会对其他任何点造出影响。

根据上述讨论,第一种构造方法的基本思想就是:自底向上执行拓扑排序,不断尝试选择零出度点。

1. 把强连通分量缩点,因为一般的拓扑排序是自顶向下根据入度进行的,所以我们建立一张缩点后的反图,具体来说:
    (1) 图上每个点都对应原图的强连通分量
    (2) 原图中的边 (x, y) 转化为新图中的边 (c[y], c[x]),其中 c[x] != c[y],c[x] 表示节点 x 所在的强连通分量
    (3) 对于原图中每个点 x,c[x] 和 c[x + N] 新图中两个对称的节点。
2. 在上述反图上统计每个点的入度,执行拓扑排序

    设val[k] 表示原图 k 号强连通分量的赋值标记,初始值为 -1

    从队头每取出一个节点 k (k 相当于原图中一个强连通分量的编号),就检查 k 的赋值标记,若 val[k] = -1 (尚未确定赋值),
    就令 val[k] = 0, val[k + N] = 1,随后把它能到达的点的入度减 1 (拓扑排序的正常过程)

3. 拓扑排序结束之后,就得到最终的答案,对于原图每个节点 i:
    若 val[c[i]] = 0,则变量 x[i] 应赋值为 x[i][0]
    若 val[c[i]] = 1,则变量 x[i] 应赋值为 x[i][1]

第二种构造方法

第二种构造方法是基于第一种构造方法的基础上,进一步利用了 Tarjan 算法对强连通分量编号的特殊性质,使得构造过程更简洁。
可以发现 Tarjan 算法的本质是一次 dfs,它在回溯时会先取出有向图底部的强连通分量进行标记,所以 Tarjan 算法得到的强连
通分量编号本身就已经满足缩点后有向无环图中自底向上的顺序,序列 1, 2, ..., cnt 就是缩点后的反图的拓扑序,无需进行拓
扑排序。

因此,直接比较节点所在的强连通分量的编号大小,即可确定对应变量的赋值 val[i],c[i] 和 c[i + N] 哪个小,就表示哪个会
先被遍历到,先被遍历到的是取值为 0,后被遍历到的取值为 1。

最终,同样的枚举一遍所有节点,根据 val[i] 来确定每个节点的值。

注:构造方法2 其实等于是 构造方法1 的升级版

作者:小小_88
链接:https://www.acwing.com/solution/content/112932/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<math.h>
#include<map>
#include<sstream>
#include<deque>
#include<unordered_map>
#include<unordered_set>
#include<bitset>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> PII;
const int N = 2e3 + 10, M = 4e6 + 10, INF = 0x3f3f3f3f;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], ts;
int id[N], cnt;
int stk[N], top;
bool in_stk[N];
struct we {
	int S, T, D;
}W[N];

void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool is_overlap(int a, int b, int c, int d) {
	if (b <= c || d <= a)return 0;
	return 1;
}

void tarjan(int u) {
	dfn[u] = low[u] = ++ts;
	stk[++top]=u, in_stk[u] = 1;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int j = e[i];
		if (!dfn[j]) {
			tarjan(j);
			low[u] = min(low[u], low[j]);
		}
		else if (in_stk[j])low[u] = min(low[u], dfn[j]);
	}
	if (dfn[u] == low[u]) {
		int y;
		cnt++;
		do {
			y = stk[top--];
			id[y] = cnt;
			in_stk[y] = 0;
		} while (y != u);
	}
}

int main() {
	cin >> n;
	memset(h, -1, sizeof h);
	for (int i = 0,s,ss,t,tt,d; i < n; i++) {
		scanf("%d:%d %d:%d %d", &s, &ss, &t, &tt, &d);
		W[i] = { s * 60 + ss,t * 60 + tt,d };
	}
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < i; j++) {
			auto a = W[i], b = W[j];
			if (is_overlap(a.S, a.S + a.D, b.S, b.S + b.D))add(i, j + n), add(j, i + n);
			if (is_overlap(a.S, a.S + a.D, b.T - b.D, b.T))add(i, j), add(j + n, i + n);
			if (is_overlap(a.T - a.D, a.T, b.S, b.S + b.D))add(i + n, j + n), add(j, i);
			if (is_overlap(a.T - a.D, a.T, b.T - b.D, b.T))add(i + n, j), add(j + n, i);
		}
	}
	for (int i = 0; i < 2 * n; i++) {
		if (!dfn[i]) {
			tarjan(i);
		}
	}
	for (int i = 0; i < n; i++) {
		if (id[i] == id[i + n]) {
			cout << "NO" << endl;
			return 0;
		}
	}
	cout << "YES" << endl;
	for (int i = 0; i < n; i++) {
		int s = W[i].S, t = W[i].T, d = W[i].D;
		if (id[i] < id[i + n])
			printf("%02d:%02d %02d:%02d\n", (s) / 60, (s) % 60, (s + d) / 60, (s + d) % 60);
		else
			printf("%02d:%02d %02d:%02d\n", (t-d) / 60, (t-d) % 60, (t) / 60, (t) % 60);
	}
	return 0;
}

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值