用最少的正方形切割矩形

18

我正在尝试解决以下问题:

将一个M*N的长方形纸张切割成正方形,要求:

  1. 沿着纸张的一条边平行地切割。
  2. 切割后的纸张边长必须为整数。

当无法再次切割时,过程停止。

最少需要切割多少次,才能得到全部都是正方形的纸片?

限制条件:1 <= N <= 100 and 1 <= M <= 100。

示例:以N=1和M=2为例,答案为2,因为最少可以切割成2个正方形(在中间沿着较小的边水平切割)。

我的代码:

cin >> n >> m;

int N = min(n,m);
int M = max(n,m);
int ans = 0;

while (N != M) {
    ans++;
    int x = M - N;
    int y = N;
    M = max(x, y);
    N = min(x, y);
}

if (N == M && M != 0)
   ans++;

但我不明白这种方法有什么问题,因为它给了我一个错误的答案。

6个回答

21

我不明白为什么这个答案的赞数这么少,动态规划方法对于这个问题行不通! - Manish Sharma
3
问题陈述中说:“纸张沿与其中一条边平行的一条线被切割”。这个答案解决了另一个问题,与所提出的问题不同。 - Bharat Kul Ratan
1
这在这个示例图像中也是正确的,不是吗?并行意味着它永远不会被例如45度旋转切割(我很难想象在哪里可以使用最小值)。我认为你的意思是添加一个规则,比如“结果形状也总是一个矩形”。 - roberto tomás

15

我会将其编写为一个动态(递归)程序。

编写一个函数尝试在某个位置分割矩形,并对两个部分进行递归调用。尝试所有可能的分割并选择具有最小结果的那个。

基本情况是当两侧相等时,即输入已经是一个正方形时,结果为1。

function min_squares(m, n):

    // base case:
    if m == n: return 1

    // minimum number of squares if you split vertically:
    min_ver := min { min_squares(m, i) + min_squares(m, n-i)  |  i ∈ [1, n/2] }

    // minimum number of squares if you split horizontally:
    min_hor := min { min_squares(i, n) + min_squares(m-i, n)  |  i ∈ [1, m/2] }

    return min { min_hor, min_ver }

为了提高性能,您可以缓存递归结果:

function min_squares(m, n):

    // base case:
    if m == n: return 1

    // check if we already cached this
    if cache contains (m, n):
        return cache(m, n)

    // minimum number of squares if you split vertically:
    min_ver := min { min_squares(m, i) + min_squares(m, n-i)  |  i ∈ [1, n/2] }

    // minimum number of squares if you split horizontally:
    min_hor := min { min_squares(i, n) + min_squares(m-i, n)  |  i ∈ [1, m/2] }

    // put in cache and return
    result := min { min_hor, min_ver }
    cache(m, n) := result
    return result

在一个具体的C++实现中,您可以使用int cache[100][100]作为缓存数据结构,因为您的输入大小是有限的。将其作为静态局部变量放置,这样它将自动初始化为零。然后将0解释为“未缓存”(因为它不能是任何输入的结果)。

可能的C++实现:http://ideone.com/HbiFOH


@user3840069,我刚刚编辑了它 ;) 现在你只需要将那些用数学表达式写出来的最小值转化为循环。使用 i 进行循环并更新一个变量,以跟踪当前最小值。 - leemes
@user3840069,现在我甚至添加了一个带有结果缓存的版本。 - leemes
@PhamTrung 有趣的是,我在我的实际C++实现中做到了这一点,但当我“迁移回”伪代码时,我写错了:) -- 已更正,谢谢。 - leemes
1
@j_random_hacker 关于你的“交换以使其有序”的建议:它已经在C++解决方案中实现了。 - leemes
1
@MehmetFide 为此,该函数不仅应返回计数,还应返回宽度列表(在C++中可能作为向量;它自动将正方形数量作为vector.size())。最小函数应该比较大小并返回较小的向量(您可以使用std::min来提供适当的比较函数,例如[](const vector &a, const vector &b){ return a.size() < b.size(); })。基本情况变为return { n };即一个具有单个正方形宽度的单元素向量。 - leemes
显示剩余9条评论

10

贪心算法并非最优解。在一个6×5的矩形上,它使用了一个5×5的正方形和5个1×1的小正方形。最优解使用了2个3×3的正方形和3个2×2的小正方形。

为了得到最优解,使用动态规划。暴力递归解决方案尝试所有可能的横向和纵向首次切割,以最佳方式递归地切割两个部分。通过缓存(记忆化)每个输入的函数值,我们得到一个多项式时间的动态规划(O(m n max(m, n)))。


2
这种方法无法涵盖将矩形切割成整数大小的正方形的所有可能方式。我愿意相信它仍然能找到最优解。当起始形状为矩形时,您的算法未覆盖的解决方案看起来效率非常低下。 - kasperd
@kasperd 取决于“沿着一条线切割”是否意味着您可以在中途停止。 - David Eisenstat

2

这个问题可以通过动态规划来解决。

假设我们有一个宽度为 N 和高度为 M 的矩形。

  • 如果 (N == M),那么它就是一个正方形,不需要做任何事情。

  • 否则,我们可以将矩形分成两个较小的矩形(N - x, M)和(x,M),然后可以递归地解决。

  • 同样地,我们也可以将其分成(N, M - x)和(N, x)。

伪代码:

int[][]dp;
boolean[][]check;
int cutNeeded(int n, int m)

    if(n == m)
       return 1;
    if(check[n][m])
       return dp[n][m];
    check[n][m] = true;
    int result = n*m;
    for(int i = 1; i <= n/2; i++)
       int tmp = cutNeeded(n - i, m) + cutNeeded(i,m);
       result = min(tmp, result); 

    for(int i = 1; i <= m/2; i++)
       int tmp = cutNeeded(n , m - i) + cutNeeded(n,i);
       result = min(tmp, result); 
    return dp[n][m] = result;

你的意思是通过 result = min(tmp); 实现 result = min(result,tmp); 吗? - user3840069
@PhanTrung 还有,你的 if(already solve this problem) 是什么意思? - user3840069
@user3840069 更新了,基本上,你只需要检查是否曾经用这些参数 nm 调用过该函数?如果是,则只需返回存储在数组 dp 中的预计算值。 - Pham Trung
@user3840069 为什么?你需要一刀将矩形分成1,1和1,1吗?还是你需要计算正方形的数量? - Pham Trung
我需要计算正方形的数量。 - user3840069
@user3840069 好的,已更新,我以为你是在问需要切割的数量。 - Pham Trung

0
这里是一个贪心实现。正如@David所提到的,它并不是最优的,在某些情况下完全错误,因此最好采用动态方法(带缓存)。
def greedy(m, n):
    if m == n:
        return 1
    if m < n:
        m, n = n, m
    cuts = 0

    while n:
        cuts += m/n
        m, n = n, m % n
    return cuts

print greedy(2, 7)

这是Python中的DP尝试 导入sys

def cache(f):
    db = {}

    def wrap(*args):
        key = str(args)
        if key not in db:
            db[key] = f(*args)
        return db[key]
    return wrap


@cache
def squares(m, n):
    if m == n:
        return 1
    xcuts = sys.maxint
    ycuts = sys.maxint
    x, y = 1, 1
    while x * 2 <= n:
        xcuts = min(xcuts, squares(m, x) + squares(m, n - x))
        x += 1
    while y * 2 <= m:
        ycuts = min(ycuts, squares(y, n) + squares(m - y, n))
        y += 1
    return min(xcuts, ycuts)

-1

这本质上是经典的整数或0-1背包问题,可以使用贪心或动态规划方法来解决。您可以参考:解决整数背包问题


1
这个问题不能通过贪心算法得到最优解,我也不认为它等同于整数背包问题。 - j_random_hacker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接