Hard
题目描述
给定一个 n x m 的矩形,返回铺满这个矩形所需的最少整数边长正方形的数量。
示例 1:
输入:n = 2, m = 3
输出:3
解释:需要3个正方形来覆盖矩形。
2个(1x1的正方形)
1个(2x2的正方形)
示例 2:
输入:n = 5, m = 8
输出:5
示例 3:
输入:n = 11, m = 13
输出:6
约束条件:
1 <= n, m <= 13
提示:
- 你能使用回溯来解决这个问题吗?
- 假设你已经放置了一些正方形,下一个正方形的自然放置位置在哪里?
- 放置的正方形的最大数量将是
≤ max(n,m)。
解题思路
解题思路
这是一个经典的回溯搜索问题,需要找到用最少正方形铺满矩形的方案。
核心思路
- 状态表示:使用一个数组记录矩形每列的高度,表示当前已被铺满的区域
- 贪心策略:总是选择最左边、最低的位置放置下一个正方形
- 回溯搜索:对于每个位置,尝试放置不同大小的正方形,递归搜索最优解
- 剪枝优化:当当前使用的正方形数量已经超过目前最优解时,直接剪枝
算法流程
- 初始化高度数组,所有位置高度为0
- 找到当前最低的位置(优先选择最左边)
- 在该位置尝试放置不同大小的正方形(从大到小)
- 更新状态,递归搜索
- 回溯时恢复状态
- 使用剪枝优化:如果当前解已经不可能更优则提前返回
特殊情况处理
- 如果 n == m,直接返回 1(一个正方形即可)
- 使用记忆化或者剪枝避免重复计算
这个问题的难点在于状态空间很大,需要通过有效的剪枝来提高效率。
代码实现
class Solution {
public:
int tilingRectangle(int n, int m) {
vector<int> heights(m, 0);
int result = n * m; // 最坏情况:全用1x1正方形
backtrack(heights, n, 0, result);
return result;
}
private:
void backtrack(vector<int>& heights, int n, int squares, int& result) {
// 剪枝:当前方案已经不可能更优
if (squares >= result) return;
// 找到最低的位置
int minHeight = *min_element(heights.begin(), heights.end());
if (minHeight == n) {
result = min(result, squares);
return;
}
// 找到最左边的最低位置
int pos = 0;
while (heights[pos] != minHeight) pos++;
// 尝试放置不同大小的正方形
for (int size = min(n - minHeight, (int)heights.size() - pos); size >= 1; size--) {
// 检查是否可以放置size x size的正方形
bool canPlace = true;
for (int j = pos; j < pos + size; j++) {
if (heights[j] != minHeight) {
canPlace = false;
break;
}
}
if (!canPlace) continue;
// 放置正方形
for (int j = pos; j < pos + size; j++) {
heights[j] += size;
}
// 递归搜索
backtrack(heights, n, squares + 1, result);
// 回溯
for (int j = pos; j < pos + size; j++) {
heights[j] -= size;
}
}
}
};
class Solution:
def tilingRectangle(self, n: int, m: int) -> int:
heights = [0] * m
result = [n * m] # 使用列表来允许修改
def backtrack(squares):
# 剪枝
if squares >= result[0]:
return
# 找到最低的位置
min_height = min(heights)
if min_height == n:
result[0] = min(result[0], squares)
return
# 找到最左边的最低位置
pos = heights.index(min_height)
# 尝试放置不同大小的正方形
max_size = min(n - min_height, m - pos)
for size in range(max_size, 0, -1):
# 检查是否可以放置
can_place = True
for j in range(pos, pos + size):
if heights[j] != min_height:
can_place = False
break
if not can_place:
continue
# 放置正方形
for j in range(pos, pos + size):
heights[j] += size
# 递归搜索
backtrack(squares + 1)
# 回溯
for j in range(pos, pos + size):
heights[j] -= size
backtrack(0)
return result[0]
public class Solution {
public int TilingRectangle(int n, int m) {
int[] heights = new int[m];
int result = n * m; // 最坏情况
Backtrack(heights, n, 0, ref result);
return result;
}
private void Backtrack(int[] heights, int n, int squares, ref int result) {
// 剪枝
if (squares >= result) return;
// 找到最低的位置
int minHeight = heights.Min();
if (minHeight == n) {
result = Math.Min(result, squares);
return;
}
// 找到最左边的最低位置
int pos = 0;
while (heights[pos] != minHeight) pos++;
// 尝试放置不同大小的正方形
int maxSize = Math.Min(n - minHeight, heights.Length - pos);
for (int size = maxSize; size >= 1; size--) {
// 检查是否可以放置
bool canPlace = true;
for (int j = pos; j < pos + size; j++) {
if (heights[j] != minHeight) {
canPlace = false;
break;
}
}
if (!canPlace) continue;
// 放置正方形
for (int j = pos; j < pos + size; j++) {
heights[j] += size;
}
// 递归搜索
Backtrack(heights, n, squares + 1, ref result);
// 回溯
for (int j = pos; j < pos + size; j++) {
heights[j] -= size;
}
}
}
}
var tilingRectangle = function(n, m) {
if (n > m) [n, m] = [m, n];
if (n === 1) return m;
if (n === m) return 1;
const memo = new Map();
function dfs(state) {
const key = state.join(',');
if (memo.has(key)) return memo.get(key);
const minHeight = Math.min(...state);
if (minHeight === m) return 0;
let pos = 0;
while (state[pos] !== minHeight) pos++;
let end = pos;
while (end < n && state[end] === minHeight) end++;
let result = Infinity;
for (let size = 1; size <= Math.min(end - pos, m - minHeight); size++) {
const newState = [...state];
for (let i = pos; i < pos + size; i++) {
newState[i] += size;
}
result = Math.min(result, 1 + dfs(newState));
}
memo.set(key, result);
return result;
}
return dfs(new Array(n).fill(0));
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(4^(nm)) - 在最坏情况下需要探索指数级的状态空间,但通过剪枝可以显著减少实际搜索量 |
| 空间复杂度 | O(m + 递归深度) - 需要 O(m) 空间存储高度数组,递归深度最多为 nm |
相关题目
- . Selling Pieces of Wood (Hard)