AcWing 2199. 骑士共存问题 题解
题目分析
本题在一个(n×n)的国际象棋棋盘上,棋盘上存在一些障碍方格,骑士(马)有特定的攻击范围。需要计算在棋盘上最多能放置多少个骑士,使得它们彼此互不攻击。
解题思路
本题可以转化为最大独立集问题,通过构建网络流图,利用最小割模型来求解。将棋盘上的方格看作节点,把所有节点分为两类(例如根据横纵坐标之和的奇偶性)。源点与其中一类节点相连,边权为(1);另一类节点与汇点相连,边权为(1)。对于骑士能攻击到的方格对应的节点之间连边,边权设为无穷大。这样,用总的可放置方格数减去最小割的值,就可以得到最多能放置的互不攻击的骑士数量。
代码逐段分析
- 头文件和全局变量定义
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 40010, M = 400010, INF = 1e8;
int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];
bool g[210][210];
- 引入必要的头文件,用于输入输出、字符串操作和算法相关功能。
- 定义常量(N)(最大节点数)、(M)(最大边数)、(INF)(无穷大)。
- 变量(n)表示棋盘的大小(行数和列数),(m)表示障碍的数量,(S)为源点,(T)为汇点。
- (h)、(e)、(ne)、(idx)用于构建邻接表存储图的边,(f)数组记录边的容量。
- (q)、(d)、(cur)用于广度优先搜索(BFS)和增广路径查找。
-
(g)数组用于标记棋盘上的方格是否为障碍,
true
表示是障碍。 -
获取节点编号函数
int get(int x, int y)
{
return (x - 1) * n + y;
}
根据棋盘的坐标((x, y))计算对应的节点编号,方便在图中表示各个方格对应的节点。
- 添加边函数
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
在图中添加一条从(a)到(b)容量为(c)的边,同时添加反向边(容量为(0))用于网络流的反向增广。
- BFS函数(构建层次图)
bool bfs()
{
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if (ver == T) return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
使用BFS构建层次图,标记每个节点的层次。若能到达汇点(T),则返回(true),表示存在从源点到汇点的路径;否则返回(false)。
- 增广路径查找函数
int find(int u, int limit)
{
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i;
int ver = e[i];
if (d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if (!t) d[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
在层次图中从节点(u)开始,沿着层次递增的路径寻找增广路径。若到达汇点(T),则返回当前的流量限制(limit)。在遍历边的过程中,若满足层次关系且边有剩余容量,则递归地在子节点中寻找增广路径,并更新边的容量。若某条路径无法继续增广,则将该节点的层次标记为(-1)。
- Dinic算法函数
int dinic()
{
int r = 0, flow;
while (bfs()) while (flow = find(S, INF)) r += flow;
return r;
}
不断调用BFS和find函数,计算最大流(等价于最小割)并返回。
- 主函数
int main()
{
scanf("%d%d", &n, &m);
S = 0, T = n * n + 1;
memset(h, -1, sizeof h);
while (m -- )
{
int x, y;
scanf("%d%d", &x, &y);
g[x][y] = true;
}
- 读取棋盘大小(n)和障碍数量(m),设置源点(S = 0)和汇点(T = n * n + 1),初始化邻接表。
- 遍历每个障碍,读取其坐标((x, y)),将(g[x][y])标记为
true
。
int dx[] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[] = {1, 2, 2, 1, -1, -2, -2, -1};
定义(dx)和(dy)数组,用于表示骑士在棋盘上可攻击方向的偏移量。
int tot = 0;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
{
if (g[i][j]) continue;
if (i + j & 1)
{
add(S, get(i, j), 1);
for (int k = 0; k < 8; k ++ )
{
int x = i + dx[k], y = j + dy[k];
if (x >= 1 && x <= n && y >= 1 && y <= n && !g[x][y])
add(get(i, j), get(x, y), INF);
}
}
else add(get(i, j), T, 1);
tot ++ ;
}
- 初始化可放置方格的总数(tot = 0)。
- 遍历棋盘上的每个方格:
- 如果该方格是障碍,则跳过。
- 如果坐标((i, j))之和为奇数,从源点(S)向该方格对应的节点连边,边权为(1),同时从该节点向骑士能攻击到的非障碍方格对应的节点连边,边权设为无穷大(INF)。
- 如果坐标((i, j))之和为偶数,将该方格对应的节点与汇点(T)相连,边权为(1)。
- 每次循环将(tot)加(1),统计可放置方格的总数。
printf("%d\n", tot - dinic());
return 0;
}
调用(dinic)函数计算最小割,用可放置方格的总数(tot)减去最小割的值,得到最多能放置的互不攻击的骑士数量并输出。
时间复杂度分析
- 构建网络流图的时间复杂度为(O(n^2)),因为需要遍历棋盘上的每个方格来添加边。
- (dinic)算法的时间复杂度为(O(n^4)),这里(n)是棋盘的边长(节点数的量级为(n^2))。
- 总的时间复杂度为(O(n^4))。
空间复杂度分析
- 图的存储使用邻接表,边数最多为(O(n^2)),所以存储图的空间复杂度为(O(n^2))。
- 其他辅助数组如(q)、(d)、(cur)、(g)等,大小与棋盘大小有关,为(O(n^2))。
- 总的空间复杂度为(O(n^2))。