y总题解写的非常好,这里补充下如何将每个正方形的所有边找出来。
算法
(IDA*) 指数级别
首先要处理出每个正方形的所有边编号
这也就是这题的难点
考虑如何描述一个正方形
我们可以用三个值描述一个正方形:
正方形边长和左上角横、纵坐标
那么我们接下来要做的就是对于每组 正方形边长和左上角横、纵坐标
,找出它所有边的编号。
先扔张图:
对于所有横着的火柴,我们将其坐标定义为其左端点坐标
对于所有竖着的火柴,我们将其坐标定义为其上端点坐标
举个例子,编号为$15$和$18$的火柴的坐标都是$(2, 0)$
先考虑横着的火柴。
对于坐标为$(r, c)$的火柴,其编号即为 $\text{坐标为 (r, 0) 的火柴的编号+c}$
而所有坐标为$(r, 0)$的火柴的编号,正好构成一个 $\text{首项为1,公差为2n+1}$ 的等差数列,其中 $\text{n}$ 为网格边长
那么坐标为$(r, 0)$的火柴,编号为$1 + r (2 n + 1)$
所以坐标为$(r, c)$的火柴,编号为$1 + r (2 n + 1) + c$
再考虑竖着的火柴。
对于所有坐标为$(r, c)$的火柴,其编号为 $\text{与其坐标相同的横着的火柴的编号+n}$
所以坐标为$(r, c)$的火柴,编号为$1 + r (2 n + 1) + c + n$
然后考虑每个正方形所包含的所有火柴的横纵坐标。
对于一个左上角坐标为$(r,c)$,边长为$len$的正方形,
不难发现其四个顶点的坐标分别为$(r, c)$,$(r + len, c)$,$(r, c + len)$,$(r + len, c + len)$
再贴张图
考虑横着的火柴(以下$i$都从$0$开始)
- 对于上边从左往右第 $i$ 个火柴,其坐标为$(r, c + i)$,所以其编号为 $1 + r (2 n + 1) + c + i$
- 对于下边从左往右第 $i$ 个火柴,其坐标为$(r + len, c + i)$,所以其编号为 $1 + (r + len)(2 n + 1) + c + i$
考虑竖着的火柴
- 对于左边从上往下第 $i$ 个火柴,其坐标为$(r + i, c)$,所以其编号为 $1 + (r + i)(2 n + 1) + c + n$
- 对于右边从上往下第 $i$ 个火柴,其坐标为$(r + i, c + len)$,所以其编号为 $1 + (r + i)(2 n + 1) + c + len + n$
所以我们只需要从 $0$ 到 $len$ 枚举 $i$,然后加边即可。
然后就是IDA*啦,估价函数之类的具体操作建议参考y总题解{:target=”_blank”}
参考文献
y总题解{:target=”_blank”}
y总视频{:target=”_blank”}
C++ 代码
#include <vector>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 61; // 网格最大是 5 * 5 的,其中最多会有 5 * (5 + 1) * 2 = 60 个正方形,所以要开到 61
int n, idx; // n 为网格规模,idx 为正方形数量
int max_depth; // IDA* 的 max_depth
vector<int> square[N]; // 存每个正方形边上的火柴的编号
bool used[N]; // 存每个火柴是否已经被破坏
// 新加一个左上角坐标为 (r, c),边长为 len 的正方形
void add(int r, int c, int len)
{
int d = n << 1 | 1; // 由于用到的 2n + 1 比较多,这里先用一个变量代替掉 2n + 1
vector<int> &s = square[idx];
s.clear(); // 有多组测试数据,需要上一组数据的内容清空
for (int i = 0; i < len; i ++ )
{
s.push_back(1 + r * d + c + i); // 上边第 i 个
s.push_back(1 + (r + len) * d + c + i); // 下边第 i 个
s.push_back(1 + n + r * d + c + i * d); // 左边第 i 个
s.push_back(1 + n + r * d + c + i * d + len); // 右边第 i 个
}
idx ++ ;
}
// 判断正方形 s 是否完整
bool check(vector<int> &s)
{
for (int i = 0; i < s.size(); i ++ )
if (used[s[i]]) return false; // 如果其中有一条边已经被破坏了,那么说明不完整
return true; // 如果每条边都没被破坏,说明完整
}
// 估价函数
int f()
{
static bool backup[N]; // 由于要改动 used,需要先新建一个备份数组
memcpy(backup, used, sizeof used); // 将 used 复制到备份数组中
int res = 0;
for (int i = 0; i < idx; i ++ ) // 枚举所有正方形
if (check(square[i])) // 如果某个正方形是完整的,
{
res ++ ; // 那么 res ++ ,并将该正方形所有的边都删去
for (int j = 0; j < square[i].size(); j ++ )
used[square[i][j]] = true;
}
memcpy(used, backup, sizeof used); // 复制回来
return res;
}
// IDA*
bool dfs(int depth)
{
if (depth + f() > max_depth) return false;
for (int i = 0; i < idx; i ++ ) // 枚举所有的正方形
if (check(square[i])) // 如果第 i 个正方形还没被破坏
{
// 那么枚举该正方形的所有边编号,去掉该边并继续爆搜
for (int j = 0; j < square[i].size(); j ++ )
{
used[square[i][j]] = true;
if (dfs(depth + 1)) return true;
used[square[i][j]] = false;
}
// 如果每条边都爆搜不成功,那么说明删掉 max_depth 个火柴无法破坏该正方形
return false;
}
return true; // 如果所有的正方形都被破坏了,返回 true
}
int main()
{
int T;
scanf("%d", &T);
while (T -- )
{
scanf("%d", &n), idx = 0; // 初始化 idx
memset(used, false, sizeof used); // 初始化 used
for (int len = 1; len <= n; len ++ ) // 枚举 len, r, c,预处理每个正方形
for (int r = 0; r + len <= n; r ++ )
for (int c = 0; c + len <= n; c ++ )
add(r, c, len);
int k;
scanf("%d", &k);
while (k -- ) // 读入所有已经被破坏的边
{
int x;
scanf("%d", &x);
used[x] = true;
}
max_depth = 0; // IDA*
while (!dfs(0)) max_depth ++ ;
printf("%d\n", max_depth);
}
return 0;
}
%%%
bool check(vector[HTML_REMOVED] &s) 这里为什么需要用&s 而不是直接s
tql %%%
讲得很清楚
666
tql