骑士放置 题解(二分图)
题目
给定一个 \(N \times M\) 的棋盘,有一些格子禁止放棋子。问棋盘上最多能放多少个不能互相攻击的骑士(国际象棋的“骑士”,类似于中国象棋的“马”,按照“日”字攻击,但没有中国象棋“别马腿”的规则)。
\(N\),\(M \leq 100\)。
分析
如果我们将棋盘黑白染色,可以发现:如果在某一个点放置骑士,那么它能够攻击到的点的颜色一定和它相反。
于是满足二分图性质,可以将黑点白点各作为二分图的左右集合。
在实际打代码时,我们并不需要真的将棋盘染色,只需要知道有这个性质就可以了。
接下来我们将每一个点与它能够到达的点连边。
我们发现:如果存在两个骑士会互相攻击,当且仅当其中某一个骑士与另一骑士能够攻击到的点位置相同。
于是要想满足题目条件,我们就需要在二分图上选择最多的点,使得任意两点之间都没有一条边相连。
即这个二分图的最大独立集。
这里引入一个定理:
设 \(G\) 是有 \(n\) 个节点的二分图,\(G\) 的最大独立集大大小等于 \(n\) 减去最大匹配
证明网上都有,实际上也并不复杂,自己画个图就能理解。
于是这道题转化为一道传统的求二分图最大匹配。
细节
我发现部分题解在解释 n*m-t-ans
时并不是很清楚
实际上 -t
可以这么理解:
定理中说的 \(n\) 个节点在这道题实际上并不是 \(N\times M\) 个点,因为障碍的点是没有连边的
于是真正存在在二分图里的点仅仅只有 \(N\times M-t\) 个点
code
#include<bits/stdc++.h>
#define Hash(i,j) (i*m+j)
using namespace std;
const int N=1e5+10;
const int maxn=110;
struct node
{
int nex;
int to;
}a[N];
int tot,head[N];
void add(int u,int v)
{
a[++tot].nex=head[u];
head[u]=tot;
a[tot].to=v;
}
int g[N];//记录障碍物
int n,m,t;
int fx[10]={-2,-1,1,2,2,1,-1,-2};//骑士能够走到的点打表(fx和fy),遍历每一个点
int fy[10]={-1,-2,-2,-1,1,2,2,1};
int vis[N];
int match[N];
bool dfs(int u)//匈牙利算法
{
for(int i=head[u];i;i=a[i].nex)
{
int v=a[i].to;
if(vis[v]) continue;
vis[v]=1;
if(!match[v]||dfs(match[v])) return match[v]=u,1;
}
return 0;
}
int main()
{
scanf("%d%d%d",&n,&m,&t);
for(int i=1,x,y;i<=t;i++)
{
scanf("%d%d",&x,&y);
g[Hash(x,y)]=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(g[Hash(i,j)]) continue;//障碍物不算
for(int k=0;k<8;k++)
{
int xx=i+fx[k];
int yy=j+fy[k];
if(xx<1||yy<1||xx>n||yy>m||g[Hash(xx,yy)]) continue;
add(Hash(i,j),Hash(xx,yy));
}
}
}
int ans=0;
for(int i=Hash(1,1);i<=Hash(n,m);i++)//求二分图最大匹配
{
if(g[i]) continue;//障碍物不管
memset(vis,0,sizeof vis);
if(dfs(i)) ans++;
}
printf("%d",n*m-t-ans/2);//因为点会算重,所以ans会除以2
return 0;
}