二分图
二分图
定义
对于一张无向图 \(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,用最少的不相交的路径覆盖所有点。其中不相交是指一个点恰好在一条路径上。
解决模型
- 将原图中的 \(n\) 个点拆成出点 \(O_i\) 和入点 \(I_i\)。
- 原图中一条 \(x \to y\) 的有向边在新图中改为 \(O_x \to I_y\)。
- 对于新图,跑二分图最大匹配,记为 \(cnt\),最少路径数为 \(n - cnt\)。
证明
- 原图中的一条路径只有一个起点和一个终点。
- 找路径数就是找终点数,也就是初度为 \(0\) 的点数。
- 左部 \(n\) 个出点,每参与一个匹配就意味着有出度。
- 当左部点匹配最多,剩下的出度为 \(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\) 直接相连的点肯定更多。
推论
二分图最大匹配为:
例题
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 定理刻画,可得:
时,无解。
则用线段树维护最大子段和即可。
代码:
#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\)。
例题
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;
}