一、题目
还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能
使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴
都要用到
。
输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼
成正方形。
注意
:
1.
给定的火柴长度和在 0 到 10^9之间。
2.
火柴数组的长度不超过15。
二、解题思路
这道题目要求我们判断是否可以使用所有的火柴拼接成一个正方形,且不允许折断火柴。所有火柴都必须被使用。
首先,我们需要计算所有火柴的总长度,并检查这个总长度是否是4的倍数。如果不是4的倍数,那么我们可以立即返回 `false`,因为这意味着无法将火柴均匀分配到四条边上,从而不可能拼成正方形。如果总长度是4的倍数,我们继续进行下一步。
接下来,我们将每一根火柴尝试放置在四条边中的任意一条上。通过这种方式,我们尝试找到一种排列,使得每条边的长度相等,从而构成一个正方形。如果最终我们能够成功地将所有火柴分配到四条边上并形成正方形,我们就返回 `true`。下面看看思路图
这就是回溯算法,他不断的尝试,一旦成功也就成功了。如果不成功,在回到上一步继续
尝试,其实他有一个经典的模板
void backtrack(/*原始参数*/) {
// 终止条件(递归必须要有终止条件)
if (/*终止条件*/) {
// 一些逻辑操作(可有可无,视情况而定)
return;
}
for (int i = /*for循环开始的参数*/; i < /*for循环结束的参数*/; i++) {
// 一些逻辑操作(可有可无,视情况而定)
// 做出选择
// 递归
backtrack(/*新的参数*/);
// 一些逻辑操作(可有可无,视情况而定)
// 撤销选择
}
}
三、代码实现
让我们来看看代码实现:
#include <vector>
using namespace std;
bool backtrack(const vector<int>& nums, int index, int target, vector<int>& size);
bool makesquare(vector<int>& nums) {
int total = 0;
// 统计所有火柴的长度
for (int num : nums) {
total += num;
}
// 如果所有火柴的长度不是4的倍数,直接返回false
if (total == 0 || (total & 3) != 0)
return false;
// 回溯
vector<int> size(4, 0); // 初始化长度为4的数组,初始值为0
return backtrack(nums, 0, total >> 2, size);
}
// index表示访问到当前火柴的位置,target表示正方形的边长,size是长度为4的数组,
// 分别保存正方形4个边的长度
bool backtrack(const vector<int>& nums, int index, int target, vector<int>& size) {
if (index == nums.size()) {
// 如果火柴都访问完了,并且size的4个边的长度都相等,说明是正方形,直接返回true,
// 否则返回false
if (size[0] == size[1] && size[1] == size[2] && size[2] == size[3])
return true;
return false;
}
// 到这一步说明火柴还没访问完
for (int i = 0; i < size.size(); i++) {
// 如果把当前火柴放到size[i]这个边上,他的长度大于target,我们直接跳过
if (size[i] + nums[index] > target)
continue;
// 如果当前火柴放到size[i]这个边上,长度不大于target,我们就放上面
size[i] += nums[index];
// 然后在放下一个火柴,如果最终能变成正方形,直接返回true
if (backtrack(nums, index + 1, target, size))
return true;
// 如果当前火柴放到size[i]这个边上,最终不能构成正方形,我们就把他从
// size[i]这个边上给移除,然后在试其他的边
size[i] -= nums[index];
}
// 如果不能构成正方形,直接返回false
return false;
}
代码优化
如果数组前面数组比较小,这会导致递归的比较深,所以我们可以先对数组进行排序,从大的开始递归,代码如下
#include <iostream>
#include <vector>
#include <algorithm> // for std::sort
using namespace std;
bool backtrack(const vector<int>& nums, int index, int target, vector<int>& size);
bool makesquare(vector<int>& nums) {
int total = 0;
// 统计所有火柴的长度
for (int num : nums) {
total += num;
}
// 如果所有火柴的长度不是4的倍数,直接返回false
if (total == 0 || (total & 3) != 0)
return false;
// 先排序
sort(nums.begin(), nums.end());
// 回溯,从最长的火柴开始
vector<int> size(4, 0); // 初始化长度为4的数组,初始值为0
return backtrack(nums, nums.size() - 1, total >> 2, size);
}
// index表示访问到当前火柴的位置,target表示正方形的边长,size是长度为4的数组,
// 分别保存正方形4个边的长度
bool backtrack(const vector<int>& nums, int index, int target, vector<int>& size) {
if (index == -1) {
// 如果火柴都访问完了,并且size的4个边的长度都相等,说明是正方形,直接返回true,
// 否则返回false
if (size[0] == size[1] && size[1] == size[2] && size[2] == size[3])
return true;
return false;
}
// 到这一步说明火柴还没访问完
for (int i = 0; i < size.size(); i++) {
// 如果把当前火柴放到size[i]这个边上,他的长度大于target,我们直接跳过。或者
// size[i] == size[i - 1]即上一个分支的值和当前分支的一样,上一个分支没有成功,
// 说明这个分支也不会成功,直接跳过即可。
if (size[i] + nums[index] > target || (i > 0 && size[i] == size[i - 1]))
continue;
// 如果当前火柴放到size[i]这个边上,长度不大于target,我们就放上面
size[i] += nums[index];
// 然后在放下一个火柴,如果最终能变成正方形,直接返回true
if (backtrack(nums, index - 1, target, size))
return true;
// 如果当前火柴放到size[i]这个边上,最终不能构成正方形,我们就把他从
// size[i]这个边上给移除,然后在试其他的边
size[i] -= nums[index];
}
// 如果不能构成正方形,直接返回false
return false;
}
int main() {
vector<int> nums = {1, 1, 2, 2, 2}; // 示例输入
bool result = makesquare(nums);
if (result) {
cout << "可以拼成正方形" << endl;
} else {
cout << "不能拼成正方形" << endl;
}
return 0;
}
回溯算法拥有一个经典的实现框架,其核心思想在于持续地进行尝试。每当遇到一条无法继续的路径时,算法便会回溯到上一步骤,并尝试其他可能的路径。上述代码已配备了详尽的注释,使得理解其运作方式变得尤为容易。