二分图

二分图

定义

对于一张无向图 \(G\),若所有点可以分为两个点集 \(A\)\(B\),且 \(A\)\(B\) 的内部没有连边,那么我们称 \(G\) 可以划分为一张二分图。

  • 二分图的划分不唯一,也不一定联通,也不一定有环。

存在的充要条件

若无向图 \(G\) 是二分图,那么 \(G\) 没有奇环。

若无向图 \(G\) 没有奇环,那么 \(G\) 是二分图。


判定

染色法。

枚举没有访问的点 \(x\) 开始 dfs,遍历点时用 \(0\)\(1\) 两种颜色交替给节点染色。若出现一个点 \(x\) 的邻接点 \(y\) 染色过且 \(col_x = col_y\),则出现奇环,不是二分图。

所有点染色成功,则是一张二分图。


代码:

inline bool dfs(int x, int Col) {
	col[x] = Col;
	
	for(auto u : g[x]) {
		if(col[x] == col[u]) return false;
		
		if(! col[u]) return dfs(u, -Col);
	}
	
	return true;
}

例题

UVA10004 Bicoloring

模板题。


P1525 [NOIP2010 提高组] 关押罪犯

将监狱看成二分图的左右两部。

由于题目要求最大影响力最小,不难想到二分答案。又因为二分图有其判定算法,所以可以很好的结合二分。

二分当前的答案,将影响力大于当前二分值的关系建边(此处值得仔细思考),二分图判定即可。


代码:

inline bool dfs(int x, int Col) {
	col[x] = Col;
	
	for(auto u : g[x]) {
		if(col[x] == col[u]) return false;
		
		else if(! col[u] && dfs(u, -Col) == false) return false;
	}
	
	return true;
}

inline bool check(int x) {
	for(int i = 1 ; i <= n ; ++ i)
		col[i] = 0, g[i].clear();
	
	for(int i = 1 ; i <= m ; ++ i)
		if(w[i] > x) g[u[i]].pb(v[i]), g[v[i]].pb(u[i]);
	
	for(int i = 1 ; i <= n ; ++ i)
		if(! col[i])
			if(! dfs(i, 1)) return false;
	
	return true;
}

匹配

定义

对于一张二分图 \(G\),一个匹配指的是一条边。


最大匹配

定义

选取的最大边集 \(E\)\(E\) 中任意两个边不共点,则称 \(E\)\(G\) 的一组最大匹配。


性质

  • 不一定唯一。
  • 任意一条边一定在不同点集内。
  • 有一些点和边必选,有一些可选。

求取:匈牙利算法(Hungarian Algorithm)

1.枚举左部的点 \(x\) 从未访问的点出发 dfs。
2.若到达右部的点 \(y\),且 \(y\) 尚未匹配(\(mch_y = 0\)),则 \(y\)\(x\) 匹配,终止 dfs。
3.若点 \(y\) 已经匹配(\(mch_y \not = 0\)),则从 \(mch_y\) 继续 dfs。
4.重复执行 \(2,3\),直到所有路径搜索完毕证明最大匹配无法增加。
5.时间复杂度 \(O(nm)\)


代码:

inline bool hungary(int x) {
	for(auto u : g[x]) {
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || hungary(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	}
	
	return false;
}

注意建的是单向边。


建模特征

不共点,边最多。


例题

P3386 【模板】二分图最大匹配

模板题。


P10937 車的放置

好题。

难点在于将行、列看成二分图的左部与右部节点,把车看作一条边。


代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 205;
int n, m, t, x, y, mch[N];
bool vis[N], ok[N][N];
vector<int> g[N];

inline bool hungary(int x) {
	for(auto u : g[x])
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || hungary(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	
	return false;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m >> t;
	for(int i = 1 ; i <= t ; ++ i) {
		cin >> x >> y;
		
		ok[x][y] = true;
	}
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			if(! ok[i][j]) g[i].pb(j);
	
	int ans = 0;
	
	for(int i = 1 ; i <= n ; ++ i) {
		memset(vis, false, sizeof vis);
		
		if(hungary(i)) ++ ans;
	}
	
	cout << ans;
	
	return 0;
}

P2055 [ZJOI2009] 假期的宿舍

一定要注意的是只有在校生有床,回家的人不需要床


代码:

for(int i = 1 ; i <= n ; ++ i) {
	for(int j = 1 ; j <= n ; ++ j) {
		cin >> x;
		
		if(x && iss[j]) g[i].pb(j);
		else if(iss[i] && ! ish[i] && i == j) g[i].pb(i);
	}
}

for(int i = 1 ; i <= n ; ++ i)
	if((iss[i] && ! ish[i]) || ! iss[i]) ++ res;

int ans = 0;

for(int i = 1 ; i <= n ; ++ i) {
	memset(vis, false, sizeof vis);
	
	if(((iss[i] && ! ish[i]) || ! iss[i]) && hungary(i)) ++ ans;
}

if(ans >= res) cout << "^_^\n";
else cout << "T_T\n";

P2756 飞行员配对方案问题

注意 \(x\) 是右部的点,而 \(mch_x\) 存的是左部的点。

分辨清楚自己将什么当作左部什么当作右部。


代码:

for(int i = 1 ; i <= n ; ++ i) {
	if(ok[mch[i]] || ! mch[i]) continue;

	ok[mch[i]] = true;

	cout << mch[i] << ' ' << i + m << '\n';
}

P2319 [HNOI2006] 超级英雄

一个不同寻常的建模方法。

将要求的答案作为一个点集去进行最大匹配,以后建模时要注意考虑到这一点。


代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 1e3 + 5;
int n, m, u, v, mch[N], Ans[N];
bool vis[N];
vector<int> g[N];
inline bool dfs(int x) {
	for(auto u : g[x]) {
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || dfs(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	}
	
	return false;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m;
	for(int i = 1 ; i <= m ; ++ i) {
		cin >> u >> v;
		
		++ u, ++ v;
		
		g[i].pb(u), g[i].pb(v);
	}
	
	int ans = 0;
	
	for(int i = 1 ; i <= m ; ++ i) {
		memset(vis, false, sizeof vis);
		
		if(dfs(i)) ++ ans;
		else break;
	}
	
	cout << ans << '\n';
	
	for(int i = 1 ; i <= n ; ++ i)
		Ans[mch[i]] = i;
	
	for(int i = 1 ; i <= ans ; ++ i)
		cout << Ans[i] - 1 << '\n'; 
	
	return 0;
}

最小点覆盖

定义

对于一张二分图 \(G\),选取最少的点覆盖所有的边,选出的点的数量即为最小点覆盖大小。


性质

  • \(G\) 中任意一条边至少有一个端点在点集内。
  • 点集选取不一定唯一。
  • 可能存在一条边的两个端点都在点集内。
  • 最小点覆盖大小 = 最大匹配大小

对于性质 \(4\) 的证明

假设最小点覆盖大小为 \(S\),最大匹配大小为 \(E\)

由于 \(E\) 条边不共点,那么覆盖 \(E\) 条边至少需要 \(E\) 个点 \(\implies S \geq E\)

在点集内每个点都可以组成一个匹配,共 \(S\) 个,得到大小为 \(S\) 的匹配 \(\implies S \leq E\)

取交集,得:\(S = E\)

证毕。


建模特征

“至少二选一”


例题

UVA1194 Machine Schedule

1.每个人物的两个模式至少有一个需要重启得到

2.重启次数最少,等价于选取模式最少

3.用最少的模式覆盖所有的任务 + 至少二选一 —— 最小点覆盖

4.注意工作模式可以为 \(0\) 的任务不参与建模


代码:

for(int i = 1 ; i <= n ; ++ i)
	mch[i] = 0, g[i].clear();

cin >> n;

if(! n) return 0;

cin >> m >> k;

for(int i = 1 ; i <= k ; ++ i) {
	cin >> u >> u >> v;

	if(u && v) g[u].pb(v);
}

int ans = 0;

for(int i = 1 ; i < n ; ++ i) {
	memset(vis, false, sizeof vis);

	if(dfs(i)) ++ ans;
}

cout << ans << '\n';

P6062 [USACO05JAN] Muddy Fields G

题意:不能盖住草地,需要盖住所有泥地,允许重叠。

1.木板长度要尽可能的少,长度就要尽可能的长(反证)。
2.木板分为横向和纵向,长度取决于连续泥地的数量。
3.一块泥地需要被横向或(而且)纵向的木板盖住,至少二选一。
4.把行和列视为二分图的左部和右部,每一块泥地视为边。
5.预处理横向和纵向的泥地连通块,并编号。
6.连通块之间建边即可跑最小点覆盖即可。


代码:

tot = 1;

for(int i = 1 ; i <= n ; ++ i)
	for(int j = 1 ; j <= m + 1 ; ++ j)
		if(c[i][j] == '*') id[i][j][0] = tot; 
		else ++ tot;


int p = tot - 1;

for(int j = 1 ; j <= m ; ++ j)
	for(int i = 1 ; i <= n + 1 ; ++ i)
		if(c[i][j] == '*') id[i][j][1] = tot; 
		else ++ tot;

for(int i = 1 ; i <= n ; ++ i)
	for(int j = 1 ; j <= m ; ++ j)
		if(c[i][j] == '*') g[id[i][j][0]].pb(id[i][j][1]);

int ans = 0;

for(int i = 1 ; i <= p ; ++ i) {
	memset(vis, false, sizeof vis);

	if(dfs(i)) ++ ans;
}

DAG 的最少无交路径覆盖

定义

对于 DAG,用最少的不相交的路径覆盖所有点。其中不相交是指一个点恰好在一条路径上。


解决模型

  1. 将原图中的 \(n\) 个点拆成出点 \(O_i\) 和入点 \(I_i\)
  2. 原图中一条 \(x \to y\) 的有向边在新图中改为 \(O_x \to I_y\)
  3. 对于新图,跑二分图最大匹配,记为 \(cnt\),最少路径数为 \(n - cnt\)

证明

  1. 原图中的一条路径只有一个起点和一个终点。
  2. 找路径数就是找终点数,也就是初度为 \(0\) 的点数。
  3. 左部 \(n\) 个出点,每参与一个匹配就意味着有出度。
  4. 当左部点匹配最多,剩下的出度为 \(0\) 的就越少。

例题

P2764 最小路径覆盖问题

对于方案的输出,一直增广指导不存在匹配点即可。


代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 155;
int n, m, u, v, mch[N], Mch[N];
bool vis[N];
vector<int> g[N];

inline bool dfs(int x) {
	for(auto u : g[x])
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || dfs(mch[u])) {
				mch[u] = x;
				Mch[x] = u;
				
				return true;
			}
		}
	
	return false;
}

inline void print(int x) {
	if(! x) return ;
	
	print(mch[x]);
	
	cout << x << ' ';
	
	return ;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m;
	for(int i = 1 ; i <= m ; ++ i) {
		cin >> u >> v;
		
		g[u].pb(v);
	}
	
	int ans = 0;
	
	for(int i = 1 ; i <= n ; ++ i) {
		memset(vis, false, sizeof vis);
		
		if(dfs(i)) ++ ans;
	}
	
	for(int i = 1 ; i <= n ; ++ i)
		if(! Mch[i]) {
			print(i);
			
			cout << '\n';
		}
	
	cout << n - ans;
	
	return 0;
}

DAG 的最少有交路径覆盖

将原图跑传递闭包后重新连边,按无交的办法去处理即可。


例题

P10938 Vani和Cl2捉迷藏


题意:

最小可交路径点覆盖。

前置知识:

最小无交路径点覆盖。

令最小无交路径点覆盖数为 \(x\),二分图最大匹配数为 \(y\),图总点数为 \(n\)

结论是:\(x = n - y\)

思路:

考虑转化。如果一个点可以间接到达另一个点,那么在图上建一条直接连接两点的边,做最小无交路径点覆盖就相当于原先的最小可交路径点覆盖。

这个证明是很好想的,可以自行思考。

对于判断一个点是否可以间接到达另一个点,用 Floyd 做传递闭包即可。


代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 205;
int n, m, u, v, mch[N], dis[N][N];
bool vis[N];
vector<int> g[N];

inline bool dfs(int x) {
	for(auto u : g[x])
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || dfs(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	
	return false;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m;
	for(int i = 1 ; i <= m ; ++ i) {
		cin >> u >> v;
		
		dis[u][v] = true;
	}
	
	for(int k = 1 ; k <= n ; ++ k)
		for(int i = 1 ; i <= n ; ++ i)
			for(int j = 1 ; j <= n ; ++ j)
				dis[i][j] |= (dis[i][k] & dis[k][j]);
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= n ; ++ j)
			if(dis[i][j]) g[i].pb(j);
	
	int ans = 0;
	
	for(int i = 1 ; i <= n ; ++ i) {
		memset(vis, false, sizeof vis);
		
		if(dfs(i)) ++ ans;
	}
	
	cout << n - ans;
	
	return 0;
}

Hall 定理

参考了这篇


定义

在二分图中,左部点集为 \(V_1\),右部点集为 \(V_2\),不妨设 \(|V_1| \le |V_2|\)

\(N(S)\) 表示点 \(S\) 相邻点的个数。

则:当且仅当 \(\forall V_0 \subseteq V_1, |V_0| \le |N(V_0)|\),二分图最大匹配为 \(|V_1|\) 时。

\(∣V_1∣ = ∣V_2∣\),则此时存在完美匹配。

注意到这个条件显然是充要条件,因为存在完美匹配时对每个 \(V_0 \subseteq V_1\),只是与 \(V_0\) 匹配的点就也有 \(∣V_0∣\) 个;与 \(V_0\) 直接相连的点肯定更多。


推论

二分图最大匹配为:

\[|V_1| - \max_{V_0 \subseteq V_1} \left \{ |V_0| - |N(V_0)| \right \} \]


例题

AT_arc076_d [ARC076F] Exhausted?

暴力建图一定会炸,考虑用 Hall 定理刻画二分图形态。

\(N(S)\) 是一定比 \(S\) 要好刻画的。由题面可得,\(N(S)\) 一定是一段前缀加上一段后缀。如果我们把 \(S\) 按左端点排序,那么最优的 \(S\) 也变成了一段前缀。于是直接二维数点即可。


代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 2e5 + 5;
int n, m;
struct Node {
    int l, r;
} a[N];

namespace SGT {
    #define mid ((L + R) >> 1)
    #define son p, L, r
    #define lson ls(p), L, mid
    #define rson rs(p), mid + 1, R

    int mx[N << 2], pls[N << 2];

    inline int ls(int p) {
        return p << 1;
    }

    inline int rs(int p) {
        return p << 1 | 1;
    }

    inline void psup(int p) {
        mx[p] = max(mx[ls(p)], mx[rs(p)]);

        return ;
    }

    inline void build(int p = 1, int L = 1, int R = m + 1) {
        if(L == R) {
            mx[p] = R - m - 1;

            return ;
        }

        build(lson), build(rson), psup(p);

        return ;
    }

    inline void work(int p, int k) {
        mx[p] += k;
        pls[p] += k;

        return ;
    }

    inline void psd(int p, int L, int R) {
        if(! pls[p]) return ;

        work(ls(p), pls[p]), work(rs(p), pls[p]);

        pls[p] = 0;

        return ;
    }

    inline void add(int l, int r, int k, int p = 1, int L = 1, int R = m + 1) {
        if(l <= L && R <= r) {
            work(p, k);

            return ;
        }

        psd(son);

        if(l <= mid) add(l, r, k, lson);
        if(r > mid) add(l, r, k, rson);

        psup(p);

        return ;
    }

    #undef mid
    #undef son
    #undef lson
    #undef rson
}

using namespace SGT;

inline bool cmp(Node a, Node b) {
    if(a.l != b.l) return a.l < b.l;

    return a.r > b.r;
}

signed main() {
    ios_base :: sync_with_stdio(NULL);
    cin.tie(nullptr);
    cout.tie(nullptr);

    cin >> n >> m;
    for(int i = 1 ; i <= n ; ++ i)
        cin >> a[i].l >> a[i].r;

    build();

    sort(a + 1, a + 1 + n, cmp);

    int pos = 1, ans = 0;

    for(int i = 0 ; i <= m + 1 ; ++ i) {
        while(pos <= n && a[pos].l == i) {
            add(1, a[pos].r, 1);
            ++ pos;
        }

        ans = max(ans, mx[1] - i);
    }

    cout << max(ans, n - m);

    return 0;
}

P3488 [POI 2009] LYZ-Ice Skates

还是考虑用 Hall 定理刻画,可得:

\[\sum_{i = 1}^{r - d} (a_i - k) > d \times k \]

时,无解。

则用线段树维护最大子段和即可。


代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 2e5 + 5;
int n, Q, k, d, x, y;

namespace SGT {
    #define mid ((L + R) >> 1)
    #define son p, L, R
    #define lson ls(p), L, mid
    #define rson rs(p), mid + 1, R

    int mx[N << 2], lmx[N << 2], rmx[N << 2], sum[N << 2];

    inline int ls(int p) {
        return p << 1;
    }

    inline int rs(int p) {
        return p << 1 | 1;
    }

    inline void psup(int p) {
        sum[p] = sum[ls(p)] + sum[rs(p)];
        lmx[p] = max(lmx[ls(p)], sum[ls(p)] + lmx[rs(p)]);
        rmx[p] = max(rmx[rs(p)], sum[rs(p)] + rmx[ls(p)]);
        mx[p] = max({lmx[p], rmx[p], mx[ls(p)], mx[rs(p)], rmx[ls(p)] + lmx[rs(p)]});

        return ;
    }

    inline void build(int p = 1, int L = 1, int R = n - d) {
        if(L == R) {
            mx[p] = sum[p] = lmx[p] = rmx[p] = -k;

            return ;
        }

        build(lson), build(rson), psup(p);

        return ;
    }

    inline void add(int x, int k, int p = 1, int L = 1, int R = n - d) {
        if(L == R) {
            mx[p] = sum[p] = lmx[p] = rmx[p] = mx[p] + k;

            return ;
        }

        if(x <= mid) add(x, k, lson);
        else add(x, k, rson);

        psup(p);

        return ;
    }

    #undef mid
    #undef son
    #undef lson
    #undef rson
}

using namespace SGT;

signed main() {
    ios_base :: sync_with_stdio(NULL);
    cin.tie(nullptr);
    cout.tie(nullptr);

    cin >> n >> Q >> k >> d;

    build();

    while(Q --) {
        cin >> x >> y;

        add(x, y);

        if(d * k < mx[1]) cout << "NIE\n";
        else cout << "TAK\n";
    }

    return 0;
}

正则二分图匹配

流程

重复 \(n\) 次:

  1. 随机选一个左边的未匹配点,然后沿增广路随机游走(即从左往右随机走未匹配边,从右往左走匹配边)直到走到一个右边的未匹配点。
  2. 把走出来的环去掉(找到最后一个出现过多次的点,然后把第一次走到它到最后一次走到它中间的这段路砍掉)。这样就找到了一条增广路。对它进行增广以把匹配数增加 \(1\)

例题

LOJ#180 正则二分图匹配

板子题。

代码:

#include <bits/stdc++.h>
#define int long long
#define pb emplace_back
using namespace std;

mt19937 rd(time(NULL));

const int N = 2e6 + 5;
int n, d, u, a[N], mch[N], mchl[N], mchr[N];
bool vis[N];
vector<int> g[N];

signed main() {
    ios_base :: sync_with_stdio(NULL);
    cin.tie(nullptr);
    cout.tie(nullptr);

    cin >> n >> d;
    for(int i = 1 ; i <= n ; ++ i) {
        mch[i] = i;

        for(int j = 1 ; j <= d ; ++ j)
            cin >> u, g[i].pb(u);
    }

    // cerr << rd() % n + 1 << ' ';
    // cerr << rd() % n + 1 << ' ';
    // cerr << rd() % n + 1 << ' ';
    // cerr << rd() % n + 1 << ' ';
    // cerr << rd() % n + 1 << ' ';

    for(int i = 1 ; i <= N ; ++ i)
        swap(mch[rd() % n + 1], mch[rd() % n + 1]);

    // for(int i = 1 ; i <= n ; ++ i)
    //     cerr << mch[i] << ' ';
    // cerr << '\n';

    // cerr << "!";

    for(int i = 1 ; i <= n ; ++ i) {
        int tot = 0, now = mch[i];
        // cerr << mch[i] << '\n';

        while(now) {
            u = g[now][rd() % d];
            while(u == mchr[now]) u = g[now][rd() % d];

            // cerr << u << ' ';

            if(vis[u]) while(a[tot] != u) vis[a[tot --]] = false;
            else {
                vis[u] = true;
                a[++ tot] = u;
            }

            // cerr << "!";

            now = mchl[u];
        }

        now = mch[i];

        // cerr << "!";

        for(int j = 1 ; j <= tot ; ++ j) {
            vis[a[j]] = false;
            mchr[now] = a[j];
            u = mchl[a[j]];
            mchl[a[j]] = now;
            now = u;
        }
        
        // cerr << "!";
    }

    for(int i = 1 ; i <= n ; ++ i)
        cout << mchr[i] << ' ';

    return 0;
}

二分图博弈

博弈模型

给定一张二分图和一个起点 \(S\),起点上有一个物品,有 \(A, B\) 两个人轮流操作。每次可以将物品沿着一条边移动,不能经过重复的点,最后不能动的人输。

结论

若起点 \(S\) 是二分图最大匹配中的必选点,先手必胜;否则,先手必败。

证明

\(S\) 是必选点时,先手总是可以走一条匹配边使得走过的路径尽可能多;而当 \(S\) 是非必选点时,无论先手怎么走,后手总是可以走另一组最大匹配内的边。

求取二分图最大匹配的必选点

沿着一个未匹配点 \(x\) 走,若点 \(x\) 的相邻匹配点 \(y\) 所对的匹配点不为 \(x\) 的话,证明点 \(y\) 是一个非必选点。再将点 \(y\) 设为未匹配点继续 dfs 即可。

例题

P4055 [JSOI2009] 游戏

网格图走四连通,可以黑白染色成二分图。

如果有完美匹配,证明先手(决定起点的人)必败。

否则,找到所有非必选点即可。

代码:

#include <bits/stdc++.h>
//#define int long long
#define pb emplace_back
using namespace std;

const int N = 105;
const int M = N * N;
const int dx[] = {0, 1, 0};
const int dy[] = {0, 0, 1};
int n, m, tot, ans, mch[M], a[N][N];
bool vis[M], can[M];
char c[N][N];
vector<int> g[M];

inline bool hungary(int x) {
	for(auto u : g[x])
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || hungary(mch[u])) {
				mch[u] = x;
				mch[x] = u;
				
				return true;
			}
		}
		
	return false;
}

inline void dfs(int x) {
	for(auto u : g[x])
		if(! vis[u]) {
			vis[u] = true;
			
			if(vis[mch[u]] || mch[u] == x || ! mch[u]) continue;
			
			can[mch[u]] = true;
			dfs(mch[u]);
		}
		
	return ;
}

signed main(){
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m;
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			cin >> c[i][j];
			
	for(int i = 1 ; i <= n ; ++ i)
		iota(a[i] + 1, a[i] + 1 + m, (i - 1) * m + 1);
		
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j) {
			if(c[i][j] == '#') continue;
			
			for(int x = 1 ; x <= 2 ; ++ x) {
				int xx = i + dx[x], yy = j + dy[x];
				
				if(xx > n || yy > m || c[xx][yy] == '#') continue;
				
				g[a[i][j]].pb(a[xx][yy]);
				g[a[xx][yy]].pb(a[i][j]);
			}
			
			++ tot;
		}
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			if((i + j) & 1 && c[i][j] != '#') {
				memset(vis, false, sizeof vis);
				
				ans += hungary(a[i][j]);
			}
			
	if(tot % 2 == 0 && ans == (tot >> 1)) return cout << "LOSE", 0;
	
	cerr << "tot:" << tot << '\n';
	
	cout << "WIN\n";
	
	memset(vis, false, sizeof vis);
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			if(! mch[a[i][j]] && c[i][j] != '#') {
				dfs(a[i][j]);
				can[a[i][j]] = true;
			}
			
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			if(can[a[i][j]]) cout << i << ' ' << j << '\n';
	
	return 0;
}
posted @ 2024-12-01 14:20  endswitch  阅读(34)  评论(1)    收藏  举报