P5182 棋盘覆盖

【题目含义】

  • 一个NxN 的棋盘(N<100)
  • 某些格子是坏的(不能放骨牌)
  • 要用1×2的骨牌覆盖所有非坏的格子
  • 骨牌不能重叠,不能覆盖坏格子
  • 问:最多能放多少块骨牌

【思考】

一块骨牌,覆盖的格子有啥特点?

对于棋盘、方格类的情况,我们可以从每个格子的坐标考虑,看是否能有发现。

一块骨牌覆盖的格子,是相邻的且横纵坐标之和一奇一偶。

关键观察

  1. 棋盘可以黑白染色
  2. 每个骨牌覆盖一黑一白两个相邻格子
  3. 坏格子相当于删去对应的顶点

标题问题:最多放多少条骨牌

→把多少个不重复的可用点连接(一黑一白)

考虑是二分图的最大匹配问题

【建图】

  • 把棋盘每个有效格子看作顶点
  • 黑格子和白格子分别作为二分图的两个部分
  • 如果两个格子相邻(上下左右)且都是好格子,则在它们之间连一条边
  • 每个骨牌对应一条匹配边

求解过程整理

  1. 每个骨牌覆盖相邻的一黑一白 → 匹配边
  2. 骨牌不重叠 → 匹配中每个顶点只被一条边使用
  3. 最大化骨牌数量 → 最大化匹配边数 匈牙利算法

注意点

1.将二维坐标 (i,j) 映射为节点编号:

常用方法:id = (i-1) * N + j,但需要区分黑白

2.国际象棋盘:(i+j)%2=0 为 黑格或者 (i+j)%2=1 为白格

两种方式都可以,只要相邻格子颜色不同

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=10009;
int a[109][109];
int match[N];
bool vis[N];
vector<int>g[N];
int n,t;
bool dfs(int x)
{

	for(int i:g[x])
	{
		if(!vis[i])
		{
			vis[i]=true;
			if(match[i]==-1 || dfs(match[i]))
			{
				match[i]=x;
				return true;
			}
		}
	}
	return false;
}
int id(int i,int j)
{
	return (i-1)*n+j;
 } 
 
bool black(int i,int j)
{
	return (i+j)%2==0;
 } 
int main()
{

	cin>>n>>t;
	while(t--)
	{
		int x,y;
		cin>>x>>y;
		a[x][y]=1;
	}
	//jiantu
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(a[i][j])   continue;
			if(!black(i,j))  continue;
			int id1=id(i,j);
			//下 
			if( i!=n && !a[i+1][j] )
			{
				int id2=id(i+1,j);
				g[id1].push_back(id2);
			}	
            //右 
			if( j!=n && !a[i][j+1] )
			{
				int id2=id(i,j+1);
				g[id1].push_back(id2);
			}
			//上
			if(i>=1 && !a[i-1][j])
			{
				int id2=id(i-1,j);
				g[id1].push_back(id2);
			 } 
			//左
			if(j >=1 && !a[i][j-1])
			{
				int id2=id(i,j-1);
				g[id1].push_back(id2);
			}	
			
			
		}
		
	}
	//匈牙利 
	memset(match,-1,sizeof match);
	int ans=0;
	
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(a[i][j])  continue;
			if(!black(i,j))  continue;
			int id1=id(i,j);
			memset(vis,0,sizeof vis);
			if(dfs(id1))
				ans++;
		}
		
	}
	cout<<ans;
	return 0;
 } 

我的锅有哪些

1.数组开小了

cpp 复制代码
const int N=109;
int a[109][109];
int match[N];
bool vis[N];
vector<int>g[N];

刚开始,我的数组开109,但是

数组 g[N], match[N], vis[N],这些是存储图节点坐标的节点标号,

而节点编号范围:id = (i-1)*n + j

当 n=100 时,最大编号:(100-1)*100 + 100 = 9900 + 100 = 10000

所以需要 N > 10000,这里取 10009

每次开数组时,要问问这个数组下标存储是是啥!!!

2. 建图只建了向右、向下的边,但实际向上、向左都要建。

cpp 复制代码
int id1=id(i,j);
//下 
if( i!=n && !a[i+1][j] )
{
		int id2=id(i+1,j);
		g[id1].push_back(id2);
}	
//右 
if( j!=n && !a[i][j+1] )
{
		int id2=id(i,j+1);
		g[id1].push_back(id2);
}

四个方向又要建

cpp 复制代码
 // 建图:黑格 -> 相邻的白格(四个方向)
    // 使用方向数组更简洁
    int dx[4] = {-1, 1, 0, 0};  // 上下左右
    int dy[4] = {0, 0, -1, 1};
    
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            if(a[i][j]) continue;          
            if(!black(i, j)) continue;     
            
            int u = id(i, j);
            
            for(int k = 0; k < 4; k++) {
                int x = i + dx[k];
                int y = j + dy[k];
                
                // 检查边界
                if(x < 1 || x > n || y < 1 || y > n) continue;
                if(a[x][y]) continue;      // 邻居是坏格子
                
                int v = id(x, y);
                g[u].push_back(v);
            }
        }
    }

3.边界问题,导致RE

cpp 复制代码
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(a[i][j])   continue;
			if(!black(i,j))  continue;
			int id1=id(i,j);
			//下 
			if(!a[i+1][j] && i!=n )
			{
				int id2=id(i+1,j);
				g[id1].push_back(id2);
			}	
            //右 
			if(!a[i][j+1] && j!=n)
			{
				int id2=id(i,j+1);
				g[id1].push_back(id2);
			}
			......

这个写法,RE这么多

问题在于

cpp 复制代码
if(!a[i+1][j] && i!=n )//要先判出界 再判断 是否是废格
......
if(!a[i][j+1] && j!=n)//要先判出界 再判断 是否是废格

当 i=n 时:

i!=n 为 false

但由于短路求值,C++会先计算 !a[i+1][j]
此时 i+1 = n+1,a[n+1][j] 数组越界!
即使 i!=n 为 false,也已经访问了非法内存

逻辑与(&&)的短路求值规则:

对于 A && B,如果 A 为 false,B 不会被计算

所以要先放边界检查,再放数组访问

【总结】
  • 边界检查要放在数组访问之前
  • 对于多维数组,要特别注意下标范围
  • 考虑使用方向数组来统一处理边界
  • 记住:先检查,再访问!

匈牙利算法中每一轮没恢复vis的值

cpp 复制代码
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(a[i][j])  continue;
			if(!black(i,j))  continue;
			int id1=id(i,j);
			//memset(vis,0,sizeof vis);这里没恢复成0
			if(dfs(id1))
				ans++;
		}
		
	}

为什么每次都要清零?

匈牙利算法寻找增广路:

对每个黑格(左部节点)尝试匹配
每次尝试都需要寻找一条增广路
vis 数组标记当前这次尝试中访问过的白格(右部节点)

举个例子

图:

  1. 第一次尝试(黑格1匹配白格1):

    vis[白格1] = true

    匹配成功:match[白格1] = 黑格1

  2. 第二次尝试(黑格2匹配):

    需要重新开始找增广路

    如果不清零,vis[白格1] 还是 true

    算法会跳过白格1,但其实应该尝试让白格1换匹配,可能错失更好的匹配方案

  3. 清零的作用

    每次 dfs(u) 前清零 vis:

    表示"重新开始一次增广路搜索"

    允许重新考虑之前匹配过的白格

    让这些白格有机会改变匹配对

4. 二维坐标转一维标号的问题

cpp 复制代码
int id(int i,int j)
{
	return i*n+j;
 } 

来看一下,id = i*n + j的编号效果

cpp 复制代码
(1,1): 1*3+1 = 4
(1,2): 1*3+2 = 5
(1,3): 1*3+3 = 6
(2,1): 2*3+1 = 7
(2,2): 2*3+2 = 8
(2,3): 2*3+3 = 9
(3,1): 3*3+1 = 10
(3,2): 3*3+2 = 11
(3,3): 3*3+3 = 12

来看一下,id = (i-1)*n + j的编号效果

cpp 复制代码
(1,1): (1-1)*3+1 = 0*3+1 = 1
(1,2): (1-1)*3+2 = 0*3+2 = 2
(1,3): (1-1)*3+3 = 0*3+3 = 3
(2,1): (2-1)*3+1 = 1*3+1 = 4
(2,2): (2-1)*3+2 = 1*3+2 = 5
(2,3): (2-1)*3+3 = 1*3+3 = 6
(3,1): (3-1)*3+1 = 2*3+1 = 7
(3,2): (3-1)*3+2 = 2*3+2 = 8
(3,3): (3-1)*3+3 = 2*3+3 = 9

综上

in+j:编号不连续,浪费空间,范围是 [n+1, n (n+1)]

(i-1)n+j:编号连续,节省空间,范围是 [1, n n]

** 建议使用:公式直观:(行号-1)*列数 + 列号 **

相关推荐
白云千载尽2 小时前
LLaMA-Factory 入门(一):Ubuntu20 下大模型微调与部署
人工智能·算法·大模型·微调·llama
yugi9878382 小时前
基于Takens嵌入定理和多种优化算法的混沌序列相空间重构MATLAB实现
算法·matlab·重构
Yuer20252 小时前
为什么要用rust做算子执行引擎
人工智能·算法·数据挖掘·rust
持梦远方2 小时前
持梦行文本编辑器(cmyfEdit):架构设计与十大核心功能实现详解
开发语言·数据结构·c++·算法·microsoft·visual studio
im_AMBER3 小时前
Leetcode 90 最佳观光组合
数据结构·c++·笔记·学习·算法·leetcode
薛不痒3 小时前
机器学习算法之SVM
算法·机器学习·支持向量机
AndrewHZ3 小时前
【复杂网络分析】如何入门Louvain算法?
python·算法·复杂网络·社区发现·community det·louvain算法·图挖掘
AndrewHZ3 小时前
【图像处理基石】如何基于黑白图片恢复出色彩?
图像处理·深度学习·算法·计算机视觉·cv·色彩恢复·deoldify
POLITE33 小时前
Leetcode 3.无重复字符的最长子串 JavaScript (Day 4)
javascript·算法·leetcode