2024.3.30 笔记
AcWing 372. 棋盘覆盖
设每个格子为 \((i,j)\)
\(i+j\) 为偶数和 \(i+j\) 为奇数的点的两个集合构成二分图的两个点集,和为偶数的边的四周全是和为奇数的点,满足二分图的性质,题目即求以和为偶数和奇数的点构成的二分图的最大匹配
const int dx[] = {0, 0, 1, -1};
const int dy[] = {1, -1, 0, 0};
int n, m;
int ans, match[M];
bool b[N][N], v[M];
vector<int> e[M];
bool dfs(int x)
{
for (unsigned int i = 0; i < e[x].size(); i++)
{
int y = e[x][i];
if (v[y]) continue;
v[y] = 1;
if (!match[y] || dfs(match[y]))
{
match[y] = x;
return 1;
}
}
return 0;
}
signed main()
{
cin >> n >> m;
while (m--)
{
int x, y;
cin >> x >> y;
b[x][y] = 1;
}
for (rint i = 1; i <= n; i++)
for (rint j = 1; j <= n; j++)
if (!b[i][j])
for (rint k = 0; k <= 3; k++)
{
int x = i + dx[k], y = j + dy[k];
if (x >= 1 && x <= n && y >= 1 && y <= n && !b[x][y])
{
e[i * n + j].push_back(x * n + y);
e[x * n + y].push_back(i * n + j);
}
}
memset(match, 0, sizeof match);
for (rint i = 1; i <= n; i++)
for (rint j = 1; j <= n; j++)
{
if ((i ^ j) & 1) continue;
memset(v, 0, sizeof v);
ans += dfs(i * n + j);
}
cout << ans << endl;
return 0;
}
AcWing 373. 車的放置
本题要求每行、每列只能放 \(1\) 个车,某个格子 \((i, j)\) 放了车,等于是占了第 \(i\) 行与第 \(j\) 行放车的名额。
因此我们可以把所有行、列看作节点,一共 \(n + m\) 个节点,如果格子 \((i, j)\) 没有被禁止,就在第 \(i\) 行
对应的节点与第 \(j\) 列对应的节点之间连无向边
对于行之间,每个车不可能同时放在两个行上,所以任意两行之间不可能有边,列同理。
要在互不冲突的前提下放置最多的车,就是求上述二分图的最大匹配数。
复杂度 \(O((N+M)*N*M)\)
int n, m, t;
int ans, match[N];
bool a[N][N], v[N];
bool dfs(int x)
{
// 不用邻接表, 直接枚举点
for (rint y = 1; y <= m; y++)
{
if (v[y] || a[x][y]) continue;
v[y] = 1;
if (!match[y] || dfs(match[y]))
{
match[y] = x;
return 1;
}
}
return 0;
}
signed main()
{
cin >> n >> m >> t;
for (rint i = 1; i <= t; i++)
{
int x, y;
cin >> x >> y;
a[x][y] = 1;
}
for (rint i = 1; i <= n; i++)
{
memset(v, 0, sizeof v);
if (dfs(i)) ans++;
}
cout << ans << endl;
}
AcWing 374. 导弹防御塔
时间较短时能击退所有入侵者,那么对于更长时间,显然也能击退入侵者,因此答案具有单调性,可以二分。
那么对于当前的二分值 \(mid\),就需要判断能否在 \(mid\) 秒之内击退所有入侵者。
已知发射预热时间 \(t1\)、冷却时间 \(t2\),我们容易计算出每座塔在 \(mid\) 分钟内最多能发射出多少枚导弹,记为 \(p\)。
可以发现,本题是一个多重匹配问题,可以把入侵者作为二分图的左部节点,每座防御塔拆成 \(p\) 个导弹,作为二分图的右部节点。
最终会有 \(m\) 个左部节点、\(n * p\) 个右部节点
注意,因为塔和入侵者的坐标各不相同,所以从塔飞到入侵者的时间也可能不同,所以在连边时,还需要检查导弹是否有足够的时间飞到入侵者。因此,一座塔的 \(p\) 个导弹是不等价的,这个多重匹配问题必须用拆点来解决。如果 \(mid\) 时间内,第 \(i\) 个入侵者能被第 \(j\) 座塔的第 \(k\) 个导弹击中,那么就在第 \(i\) 个左部节点和第 \((j - 1) * p + k\) 个右部节点之间连一条无向边。
然后用匈牙利算法求最大匹配数,若左部节点都能找到匹配,说明 mid 时间内能击退入侵者,二分左区间,否则二分右区间。
int n, m;
double t1, t2, V;
pair<int, int> a[N], b[N];
//入侵者、塔的坐标
double dist[N][N];
//每个入侵者和塔之间的距离
bool v[N * N];
int match[N * N];
vector<int> e[N];
//e[i] 存储所有能打到第i个入侵者的导弹
double get_dist(int i, int j)
//计算第i个入侵者和第j座塔之间的距离
{
double x = a[i].x - b[j].x;
double y = a[i].y - b[j].y;
return sqrt(x * x + y * y);
}
bool dfs(int x) //匹配
{
for (rint i = 0; i < e[x].size(); i++)
{
int y = e[x][i];
if(v[y]) continue;
v[y] = 1;
if(!match[y] || dfs(match[y]))
{
match[y] = x;
return 1;
}
}
return false;
}
bool check()
//判断能否击退所有入侵者
{
memset(match, 0, sizeof match);
for (rint i = 1; i <= m; i++)
//枚举所有入侵者
{
memset(v, 0, sizeof v);
if(!dfs(i)) return 0;
//只要有一个入侵者无法击退,则返回 0
}
return 1;
//到这说明所有入侵者都能击退,返回 1
}
signed main()
{
cin >> n >> m >> t1 >> t2 >> V;
t1 /= 60; //转化成分钟
for (rint i = 1; i <= m; i++) cin >> a[i].x >> a[i].y;
for (rint i = 1; i <= n; i++) cin >> b[i].x >> b[i].y;
//计算所有入侵者和塔之间的距离
for (rint i = 1; i <= m; i++)
for (rint j = 1; j <= n; j++)
dist[i][j] = get_dist(i, j);
double l = 0, r = 1e9; //二分
while (r - l > eps)
{
double mid = (l + r) / 2;
int p = (mid + t2) / (t1 + t2);
//计算在mid时间内每座塔最多能发射多少枚导弹
p = min(p, m);
//导弹数量最多只需要m枚
for (rint i = 1; i <= m; i++) //枚举每个入侵者
{
e[i].clear();
for (rint j = 1; j <= n; j++) //枚举所有塔
for (rint k = 1; k <= p; k++) //枚举每一发导弹
//如果mid时间内第j座塔的第k发导弹能击退当前入侵者,连一条边
if (k * t1 + (k - 1) * t2 + dist[i][j] / V < mid - eps)
e[i].push_back((j - 1) * p + k);
}
if (check()) r = mid; //如果mid时间内能击退所有敌人,尝试缩短时间
else l = mid; //否则需要扩大时间
}
printf("%.6lf\n", r);
return 0;
}
UVA1411 Ants
题目要求最后所有线段都不相交。
那么试想,如果第 \(i_1\) 个白点连第 \(j_1\) 个黑点,第 \(i_2\) 个白点连第 \(j_2\) 个黑点,并且线段 \((i_1, j_1)\) 和线段 \((i_2, j_2)\) 相交,
那么如果交换一下,让 \(i_1\) 连 \(j_2\),\(i_2\) 连 \(j_1\),这样两条线段就不会相交,根据三角形的性质可知,两条交换后的线段的长度
之和一定变小。
所以求所有线段的长度之和最小的方案,所有线段一定互不相交。
如果将所有白点作为左部节点,所有黑点作为右部节点,可以发现这是一个二分图,因此求的就是二分图的带权最小匹配。
由于每个白点都一定能对应一个黑点,因此是一个二分图的完备匹配,所以可以用 \(KM\) 算法。
\(KM\) 算法求的是二分图的带权的最大完备匹配,这里可以将所有边的权值取反,那么就转化为最大匹配了。
double w[N][N]; // 边权
double la[N], lb[N], upd[N]; // 左、右部点的顶标
bool va[N], vb[N]; // 访问标记:是否在交错树中
int match[N]; // 右部点匹配了哪一个左部点
int last[N]; // 右部点在交错树中的上一个右部点,用于倒推得到交错路
int n;
struct
{
int x, y;
} a[N], b[N];
bool dfs(int x, int father)
{
va[x] = 1;
for (rint y = 1; y <= n; y++)
{
if (vb[y]) continue;
if (fabs(la[x] + lb[y] - w[x][y]) < eps)
{ // 相等子图
vb[y] = 1;
last[y] = father;
if (!match[y] || dfs(match[y], y))
{
match[y] = x;
return 1;
}
}
else if (upd[y] > la[x] + lb[y] - w[x][y] + eps)
{
upd[y] = la[x] + lb[y] - w[x][y];
last[y] = father;
}
}
return 0;
}
void KM()
{
for (rint i = 1; i <= n; i++)
{
la[i] = -INT_MAX;
lb[i] = 0;
for (rint j = 1; j <= n; j++)
la[i] = max(la[i], w[i][j]);
}
for (rint i = 1; i <= n; i++)
{
memset(va, 0, sizeof va);
memset(vb, 0, sizeof vb);
for (rint j = 1; j <= n; j++)
upd[j] = INT_MAX;
// 从右部点st匹配的左部点match[st]开始dfs,一开始假设有一条0-i的匹配
int st = 0;
match[0] = i;
while (match[st])
{ // 当到达一个非匹配点st时停止
double delta = INT_MAX;
if (dfs(match[st], st)) break;
for (rint j = 1; j <= n; j++)
if (!vb[j] && delta > upd[j])
{
delta = upd[j];
st = j; // 下一次直接从最小边开始DFS
}
for (rint j = 1; j <= n; j++)
{ // 修改顶标
if (va[j]) la[j] -= delta;
if (vb[j]) lb[j] += delta;
else upd[j] -= delta;
}
vb[st] = true;
}
while (st)
{ // 倒推更新增广路
match[st] = match[last[st]];
st = last[st];
}
}
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++) cin >> b[i].x >> b[i].y;
for (rint i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
for (rint i = 1; i <= n; i++)
for (rint j = 1; j <= n; j++)
w[i][j] = -sqrt((a[i].x - b[j].x) * (a[i].x - b[j].x) + (a[i].y - b[j].y) * (a[i].y - b[j].y));
KM();
for (rint i = 1; i <= n; i++) cout << match[i] << endl;
return 0;
}
当然此题也存在费用流做法
int n, m, k, S, T;
int e[M], ne[M], f[M], h[N], idx;
double w[M];
int ans[N];
int incf[N], pre[N];
bool v[N];
double d[N];
void add(int a, int b, int c, double d)
{
e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx++;
e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx++;
}
struct node
{
int x, y;
} a[N];
double get_dis(node a, node b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
bool SPFA()
{
queue<int> q;
fill(d, d + 2 * n + 2, 1e18);
memset(v, 0, sizeof v);
memset(incf, 0, sizeof incf);
q.push(S);
d[S] = 0;
incf[S] = inf;
while (!q.empty())
{
int x = q.front();
q.pop();
v[x] = 0;
for (rint i = h[x] ; ~i; i = ne[i])
{
int y = e[i];
double z = w[i];
if (f[i] && d[y] > d[x] + z)
{
d[y] = d[x] + z;
incf[y] = min(incf[x], f[i]);
pre[y] = i;
if (!v[y])
{
q.push(y);
v[y] = 1;
}
}
}
}
return incf[T] > 0;
}
void EK()
{
while (SPFA())
{
int t = incf[T];
for (rint i = T ; i != S ; i = e[pre[i] ^ 1])
{
f[pre[i]] -= t;
f[pre[i] ^ 1] += t;
}
}
}
signed main()
{
cin >> n;
S = 0, T = 2 * n + 1;
memset(h, -1, sizeof h);
for (rint i = 1; i <= n; i++)
{
cin >> a[i].x >> a[i].y;
//从源点向所有黑点(二分图左部)连容量为1费用为0的边
add(S, i, 1, 0);
}
for (rint i = 1; i <= n; i++)
{
cin >> a[i + n].x >> a[i + n].y;
//从所有白点(二分图右部)向汇点连容量为1费用为0的边
add(i + n, T, 1, 0);
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= n; j++)
{
//从左部点向右部点连容量为1费用为距离的边
add(i, j + n, 1, get_dis(a[i], a[j + n]));
}
}
EK();
for (rint i = 1; i <= n; i++)
{
for (rint j = h[i]; ~j; j = ne[j])
{
//残留网络中流量为0表示选择了该边
if (!f[j])
{
cout << e[j] - n << endl;
break;
}
}
}
return 0;
}
UVA1194 Machine Schedule
将每个任务看成一条边,连接的两台机器为两个点
做完所有的任务就是覆盖所有的边
刚开始每台机器模式均为 \(0\)
每转换一次模式相当于选择一个新的点
则问题转化为 求最小的点覆盖所有的边
在二分图中,最小点覆盖 == 最大匹配数
将每个任务在机器 \(A\) 上设置的模式与源点 \(S\) 连一条边,在机器 \(B\) 上设置的模式与汇点 \(T\) 连一条边,在将在机器 \(A\) 上设置的模式的点 与 在机器 \(B\) 上设置的模式的点连一条边,因为每个任务只需要完成一次,所以边的容量均为 \(1\)
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
bool dfs(int x)
{
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!vis[y])
{
vis[y] = 1;
if (!match[y] || dfs(match[y]))
{
match[y] = x;
return true;
}
}
}
return false;
}
signed main()
{
int n;
// while (~scanf("%lld", &n) and n != 0)
while (114514)
{
cin >> n;
if (n == 0)
{
break;
}
int m, k;
cin >> m >> k;
memset(e, 0, sizeof e);
memset(ne, 0, sizeof ne);
memset(h, 0, sizeof h);
memset(match, 0, sizeof match);
idx = 0;
for (rint i = 1; i <= k; i++)
{
int a, b, fw;
cin >> fw >> a >> b;
if (a == 0 || b == 0)
{
continue;
}
add(a, b + n);
add(b + n, a);
}
int ans = 0;
for (rint i = 1; i <= n; i++)
{
memset(vis, 0, sizeof vis);
if (dfs(i))
{
ans++;
}
}
cout << ans << endl;
}
return 0;
}
P6062 Muddy Fields G
对于每块泥块都需要被横向或竖向的木板至少覆盖一次。
由于木板不能盖住干净的地面,所以横着的木板只能覆盖同一行若干个连续的泥块。
竖着的木板只能覆盖同一列若干个连续的泥块。我们把这些连续的泥块分别分成 行泥块 和 列泥块。
对于每个泥块,把它所在的行泥块和列泥块之间连一条边。
那么可以发现,行泥块就是左部节点,列泥块就是右部节点。我们只需要保证每条边至少有一个节点被覆盖即可。
因此求的就是二分图的最小点覆盖,等价于求最大匹配数,用匈牙利算法来解决。
const int N = 6e1 + 5;
const int M = 4e3 + 5;
int n, m, tot = 1;
int a[N][N][2], match[M], ans;
char s[N][N];
bool v[M];
vector<int> e[M];
bool dfs(int x)
{
for (unsigned int i = 0; i < e[x].size(); i++)
{
int y = e[x][i];
if (v[y]) continue;
v[y] = 1;
if (!match[y] || dfs(match[y]))
{
match[y] = x;
return 1;
}
}
return 0;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++) scanf("%s", s[i] + 1);
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= m + 1; j++)
{
if (s[i][j] == '*') a[i][j][0] = tot;
else ++tot;
}
}
int t = tot;
for (rint j = 1; j <= m; j++)
{
for (rint i = 1; i <= n + 1; i++)
{
if (s[i][j] == '*') a[i][j][1] = tot;
else ++tot;
}
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= m; j++)
{
if (s[i][j] == '*')
{
e[a[i][j][0]].push_back(a[i][j][1]);
e[a[i][j][1]].push_back(a[i][j][0]);
}
}
}
for (rint i = 1; i < t; i++)
{
memset(v, 0, sizeof v);
if(dfs(i)) ans++;
}
cout << ans << endl;
return 0;
}
AcWing 378. 骑士放置
最大独立集:在一个图中,选出最多的点,使得选出的点之间没有边。
最大团:在一个图中,选出最多的点,使得任意两点之间有边。
两者互补(补图:在一个图中,如果原来两点之间有边,则去掉该边,如果两点之间无边,则加上该边)
在二分图中,求最大独立集 \(<=>\) 求去掉最少的点将所有边破坏 \(<=>\) 找最小点覆盖 \(<=>\) 最大匹配
结论:设总点数是 \(n\),最大匹配是 \(m\),最大独立集是 \(n-m\)
该题中,将每个格子看成点,如果两个格子中放棋子之后可以攻击到则连一条边,任务就是找出最多的点互相攻击不到,既找出最多的点,使点之间没有边,即最大独立集问题,这里同样按照点的奇偶性染色,可以发现同样满足二分图的性质,所以最后的结果就是 \(n*m-\) 禁止放置点数 \(-\) 最大匹配数。
const int dx[] = {-2, -2, -1, -1, 1, 1, 2, 2};
const int dy[] = {-1, 1, -2, 2, -2, 2, -1, 1};
int n, m, t, ans;
int fx[N][N], fy[N][N];
bool a[N][N], v[N][N];
bool dfs(int x, int y)
{
for (rint i = 0; i < 8; i++)
{
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 1 || ny < 1 || nx > n || ny > m || a[nx][ny]) continue;
if (v[nx][ny]) continue;
v[nx][ny] = 1;
if (fx[nx][ny] == 0 || dfs(fx[nx][ny], fy[nx][ny]))
{
fx[nx][ny] = x;
fy[nx][ny] = y;
return 1;
}
}
return 0;
}
signed main()
{
cin >> n >> m >> t;
for (rint i = 1; i <= t; i++)
{
int x, y;
cin >> x >> y;
a[x][y] = 1;
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= m; j++)
{
if ((i + j) & 1 || a[i][j]) continue;
memset(v, 0, sizeof v);
if (dfs(i, j)) ans++;
}
}
cout << n * m - t - ans << endl;
return 0;
}
AcWing 379. 捉迷藏
考虑从路径上选点,要求每条路径只能选一个点,考虑可选路径和可选点数
记最小路径重复点覆盖为 \(cnt\),则可选路径 \(≤ cnt\),则可选点数 \(≤ cnt\)
下面构造一种选法,使点数可以等于 \(cnt\)
对于所有最小路径重复点覆盖中的路径,记终点集合为 \(E\),从 \(E\) 能到达的所有点记为 \(nxt(E)\)
若 \(E \cap nxt(E) = \varnothing\),则 \(E\) 内部两两之间不可相互到达,所以 \(E\) 可以作为一种方案,点数为 \(cnt\)
若 \(E \cap nxt(E) \neq \varnothing\),对 \(E\) 中的所有点 \(e_i\),让它一直往前走,直到 \(e_i \notin nxt(E)\) 时选择当前点,则最终选出的 \(cnt\) 个点可以作为一种方案
证明:对于任意 \(e_i \in E\),最多走到起点,就能找到 \(e_i \notin nxt(E)\)
假设存在 \(e_i \in E\),一直退到起点,路径上的点都属于 \(nxt(E)\),则起点可以被其它终点到达,把两条路径首尾相接可合并为一条路径,使总路径数变少,这与最小路径重复点覆盖的定义矛盾
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 3e2 + 5;
int n, m;
bool cl[N][N];
// 邻接矩阵
int match[N];
bool v[N], succ[N];
int hide[N];
// 藏身点集合
bool dfs(int x)
{
for (rint i = 1; i <= n; i++)
{
if (cl[x][i] && !v[i])
{
v[i] = 1;
if (!match[i] || dfs(match[i]))
{
match[i] = x;
return 1;
}
}
}
return 0;
}
signed main()
{
// 读入
cin >> n >> m;
for (rint i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
cl[x][y] = 1;
}
// Floyd 传递闭包
for (rint i = 1; i <= n; i++) cl[i][i] = 1;
for (rint k = 1; k <= n; k++)
for (rint i = 1; i <= n; i++)
for (rint j = 1; j <= n; j++)
cl[i][j] |= cl[i][k] && cl[k][j];
for (rint i = 1; i <= n; i++) cl[i][i] = 0;
// 在拆点二分图上求最大匹配
int ans = n;
for (rint i = 1; i <= n; i++)
{
memset(v, 0, sizeof v);
ans -= dfs(i);
}
cout << ans << endl;
// 构造方案,先把所有路径终点(左部非匹配点)作为藏身点
for (rint i = 1; i <= n; i++) succ[match[i]] = 1;
for (rint i = 1, k = 0; i <= n; i++)
if (!succ[i])
hide[++k] = i;
memset(v, 0, sizeof v);
bool modify = 1;
while (modify)
{
modify = 0;
// 求出 next(hide)
for (rint i = 1; i <= ans; i++)
for (rint j = 1; j <= n; j++)
if (cl[hide[i]][j])
v[j] = 1;
for (rint i = 1; i <= ans; i++)
if (v[hide[i]])
{
modify = 1;
// 不断向上移动
while (v[hide[i]])
hide[i] = match[hide[i]];
}
}
//for (rint i = 1; i <= ans; i++) cout << hide[i] << " ";
//hide 为方案数
return 0;
}