[XIX Open Cup, Grand Prix of Korea] Dev, Please Add This! 题解

前置知识:2-SAT

题意

[XIX Open Cup, Grand Prix of Korea] Dev, Please Add This!

在一张网格上有以下几种物品:

  • 空地(.
  • 墙(#
  • 星星(*
  • 一个球(O

现在你要玩一个游戏。你可以让球朝上下左右某一个方向移动,但是一旦移动就必须走到底,也就是说直到前面是墙或者边界才能停。另外,如果你走到了带有星星的格子上,你可以获得它。

你的任务是,判断一张网格是否有吃完所有星星的方案。

样例1

输入:
3 7
#..O..#
#.###.#
*..#..*

输出:
NO

不难发现没有两个星星都吃到的方案。因为一旦吃到一个星星就回不到起点了。

输入:
6 6
*..*##
..O...
*..*#.
####*.
......
.....#

输出:
YES

可以按照以下的步骤:右、下、左、下、右、上、右、上、左、上、右、下、左。

思路

首先,根据题目列出条件:

  1. 起点走不到的点不可能走到(合法性);
  2. 若两个位置相互都不可达,则这两个位置不可能都走到(合法性);
  3. 每个星星要么横着走到,要么竖着走到,不能不经过(目标)。

这时我断言,满足它们的路径存在,等价于有方案吃完所有星星。

必要性显然。充分性,先考虑一个问题:给一张无向完全图,给每条边定向使得形成一张 dag,然后需要找出一条哈密顿路径。由于是 dag,所以考虑拓扑序:对于一组合法的拓扑序,由于相邻两项必有连边,所以顺着拓扑序走就能找到一个哈密顿路径(因此拓扑序也是唯一的)。而原问题的条件显然不会强于这个问题——由于每两点都至少有一条有向边,并且连边具有传递性(1→2 且 2→3,则 1→3),所以肯定包含上述问题中那样的 dag。

接下来,看到上面的加粗部分,就容易想到 2-SAT 了:

我们对所有「一段横向或纵向的极长可行走区间」编一个号作为图上的节点,u 表示该节点走,pre + u 表示该节点不走。然后从节点每个点跑一遍 dfs(或 bfs),令 vis[u][v] 表示可以从 u 到达 v。然后那么可以转为:

  1. !vis[起点所属极长横区间][i] && !vis[起点所属极长纵区间][i],则走不到,连边 i -> pre + i
  2. !vis[u][v] && !vis[v][u],则互相都不能走到,连边 u -> pre + vv -> pre + u
  3. 若某一个格子是星星,令所属极长横区间的编号为 u,所属极长纵区间的编号为 v,则连边 pre + u -> vpre + v -> u

然后跑 2-SAT 即可。时间复杂度 \(\mathcal{O}(能过并且绰绰有余)\)

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 55, D = N * N * 2;
struct two_sat
{
    int n;
    vector<int> g[D];
    void addEdge(int u, int v)
    {
        g[u].push_back(v);
    }
    int dfncnt, dfn[D], low[D], stk[D], tp;
    bool instk[D];
    int scccnt, id[D];
    void tarjan(int u)
    {
        dfn[u] = low[u] = ++dfncnt;
        stk[++tp] = u;
        instk[u] = true;
        for (int v : g[u])
        {
            if (!dfn[v])
            {
                tarjan(v);
                low[u] = min(low[u], low[v]);
            }
            else if (instk[v])
                low[u] = min(low[u], dfn[v]);
        }
        if (dfn[u] == low[u])
        {
            ++scccnt;
            int v;
            do
            {
                v = stk[tp--];
                instk[v] = false;
                id[v] = scccnt;
            } while (v != u);
        }
    }
} t;
int n, m, id[2][N][N], st;
// id[0][x][y]表示(x,y)所属的横向极长区间
// id[1][x][y]表示(x,y)所属的纵向极长区间
char s[N][N];
pair<int, int> l[D], r[D], S;    // 每个极长区间的左右/上下端点
bool vis[D][D];                  // vis[u][v]表示u能走到v

void dfs(int u)
{
    vis[st][u] = true;
    auto tmp = l[u];
    int v = u ^ id[0][tmp.first][tmp.second] ^ id[1][tmp.first][tmp.second];
    /*
    上面那行代码等价于:
    if (u == id[0][tmp.first][tmp.second])
    	v = id[1][tmp.first][tmp.second];
    else
		v = id[0][tmp.first][tmp.second];
	下面同理。作用是分别从两个端点走,但要跟当前的方向不一样。
    */
    if (!vis[st][v])
        dfs(v);
    tmp = r[u];
    v = u ^ id[0][tmp.first][tmp.second] ^ id[1][tmp.first][tmp.second];
    if (!vis[st][v])
        dfs(v);
}

int main()
{
#ifdef aquazhao
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        scanf("%s", s[i] + 1);
    int cnt = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            if (s[i][j] == '#')
                continue;
            if (j == 1 || s[i][j - 1] == '#')
                l[++cnt] = {i, j};
            id[0][i][j] = cnt;
            if (j == m || s[i][j + 1] == '#')
                r[cnt] = {i, j};
        }
    for (int j = 1; j <= m; j++)
        for (int i = 1; i <= n; i++)
        {
            if (s[i][j] == '#')
                continue;
            if (i == 1 || s[i - 1][j] == '#')
                l[++cnt] = {i, j};
            id[1][i][j] = cnt;
            if (i == n || s[i + 1][j] == '#')
                r[cnt] = {i, j};
        }
    for (st = 1; st <= cnt; st++)
        dfs(st);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            if (s[i][j] == '*')
            {
                t.addEdge(id[0][i][j] + cnt, id[1][i][j]);
                t.addEdge(id[1][i][j] + cnt, id[0][i][j]);
            }
            else if (s[i][j] == 'O')
                S = {i, j};
        }
    for (int i = 1; i <= cnt; i++)
        if (!vis[id[0][S.first][S.second]][i] && !vis[id[1][S.first][S.second]][i])
            t.addEdge(i, i + cnt);
    for (int i = 1; i <= cnt; i++)
        for (int j = i + 1; j <= cnt; j++)
            if (!vis[i][j] && !vis[j][i])
            {
                t.addEdge(i, j + cnt);
                t.addEdge(j, i + cnt);
            }
    for (int i = 1; i <= cnt + cnt; i++)
        if (!t.dfn[i])
            t.tarjan(i);
    bool ans = true;
    for (int i = 1; i <= cnt; i++)
        if (t.id[i] == t.id[i + cnt])
        {
            ans = false;
            break;
        }
    puts(ans ? "YES" : "NO");
    return 0;
}
posted @ 2024-11-12 19:50  Aquizahv  阅读(32)  评论(0)    收藏  举报