P4055 [JSOI2009] 游戏 题解

很妙的题目,难点在于如何想到把棋子的移动转化为二分图的匹配。

对于这种每次移动只能移到相邻各自的棋盘问题,可以先对棋盘进行黑白染色,染完色后就可以发现:每次移动必定从黑格移动到白格,或从白格移动到黑格。

而且很明显的一点是:黑格和白格构成了一个二分图,相邻的格子在二分图里就有连边。

于是尝试把移动棋子和二分图结合在一起:假设棋子是在黑格里,棋子的一次移动相当于从黑点集到白点集的一条边;多次移动的话就是多条边,同时也就构成了一条链。

接下来我们只考虑从黑点集到白点集的边(即忽略白点集到黑点集的边),这样思考仍然是对的,因为一个白点若连接到了一个作为起点的黑点,那么下一步肯定是走到这个黑点的。

根据题目给出的条件:同一个格子不能进入两次。转化到这个二分图里就是:同一个点或同一条边不能经过两次。

什么时候游戏结束了呢?就是不存在一个没有作为起点的黑点与一个没有作为终点的白点之间有连边,因为这样就肯定存在一种方案使得棋子可以走到这个黑点然后再走到这个白点。

综合上面这些信息,我们可以想到二分图最大匹配。

下面开始分析,加粗的地方是比较重要或者可能有点绕,不容易理解的部分。

如果二分图有完美匹配,那么先手必胜(这里指先移动棋子的玩家),因为他永远可以将这个棋子移动到它的配对点上,而后手只能将这个棋子移动到一个其配对点没有被经过的点上或者无法移动,所以最后一步一定是先手的。

如果二分图没有完美匹配,那么后手必胜:后手将棋子放在一个不属于这个二分图的某种最大匹配的点,先手一定会移动到某个匹配点上,然后相当于回到了上面那种情况,只不过先手后手反转了而已。

先手不可能可以将棋子从非匹配点移动到非匹配点上,因为这样就和最大匹配矛盾了(此时可以在这两点之间连边)。

对于求具体方案,我们需要找到所有的不属于这个二分图的某种最大匹配的点,也就是最大匹配的非必需点。

我们可以先求出一种最大匹配,从非匹配点开始搜索,找到和非匹配点相邻的已匹配点,那么类比匈牙利算法求最大匹配,我们是可以将这个非匹配点和与其相邻的已匹配点的配对点替换的,这个时候仍然是一个合法的最大匹配,同时也将一个已匹配点换成了非匹配点。一直这么搜索下去就好了。

上面这一段分析可能会有比较模糊的地方,可以结合代码理解一下,代码里有注释:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int dx[] = {1, 0, -1, 0};
const int dy[] = {0, 1, 0, -1};
int n, m, u, v, cnt, sz, match[10005], id[105][105];
bool vis[10005];
char ch;
queue<int> q;
vector< pair<int, int> > ans;
vector<int> st, g[10005];
//匈牙利
bool dfs(int now) {
    for(const auto& i : g[now]) {
        if(vis[i]) continue;
        vis[i] = true;
        if(!match[i] || dfs(match[i])) {
            match[now] = i, match[i] = now;
            return true;
        }
    }
    return false;
}
int solve() {
    int ret = 0;
    for(const auto& i : st) {
        if(!match[i]) {
            memset(vis, false, sizeof(vis));
            if(dfs(i)) ++ret;
        }
    }
    return ret;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m;
    for(int i = 1; i <= n; ++i) {
        for(int j = 1; j <= m; ++j) {
            cin >> ch;
            if(ch == '.') id[i][j] = ++cnt;//给每个空地编号
        }
    }
    for(int i = 1; i <= n; ++i) {
        for(int j = 1; j <= m; ++j) {
            if(!id[i][j]) continue;
            if((i + j) & 1) {//只从黑格/白格开始连边
                st.push_back(id[i][j]);
                for(int k = 0; k < 4; ++k) {
                    u = i + dx[k], v = j + dy[k];
                    if(u >= 1 && u <= n && v >= 1 && v <= m && id[u][v]) {//建边
                        g[id[i][j]].push_back(id[u][v]);
                        g[id[u][v]].push_back(id[i][j]);
                    }
                }
            }
        }
    }
    if((solve() << 1) == cnt) cout << "LOSE";//如果有完美匹配的话一定会输
    else {
        cout << "WIN\n";
        while(!q.empty()) q.pop();
        memset(vis, false, sizeof(vis));
        for(int i = 1; i <= cnt; ++i) {
            if(!match[i]) {//非当前最大匹配点一定可以作为一个后手必胜的起点
                vis[i] = true;
                q.push(i);
            }
        }
        while(!q.empty()) {
            u = q.front();
            q.pop();
            for(const auto& i : g[u]) {
                if(match[i]) {
                    if(!vis[match[i]]) {//相当于把当前非匹配点和另一个已匹配的点的配对点替换,仍然是一个可行的最大匹配,所以那个点的配对点是非必需点
                        vis[match[i]] = true;
                        q.push(match[i]);
                    }
                }
            }
        }
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; j <= m; ++j) {
                if(vis[id[i][j]]) ans.push_back(make_pair(i, j));//统计答案
            }
        }
        for(const auto& i : ans) cout << i.first << " " << i.second << '\n';
    }
    return 0;
}
posted @ 2023-11-08 17:00  A_box_of_yogurt  阅读(10)  评论(0编辑  收藏  举报  来源
Document