[LeetCode] 1240. Tiling a Rectangle with the Fewest Squares 鋪瓷磚



Given a rectangle of size n x m, return the minimum number of integer-sided squares that tile the rectangle.

Example 1:

Input: n = 2, m = 3
Output: 3
Explanation: `3` squares are necessary to cover the rectangle.
`2` (squares of `1x1`)
`1` (square of `2x2`)

Example 2:

Input: n = 5, m = 8
Output: 5

Example 3:

Input: n = 11, m = 13
Output: 6

Constraints:

  • 1 <= n, m <= 13

這道題給了一個 n by m 大小的矩形,問最少可以用多少個正方形填滿這個矩形。有點像小時候玩的拼圖游戲,用大小不同正方形來拼出一個大的矩形。為了方便分析,這里始終認為n小於等於m,若給定的n大於m,直接調換一下也不影響最終結果。現在來考慮一下如何才能拼出 n by m 大小的矩形,可能大家會下意識的先拿出一個 n by n 的矩形占一大塊,然后再拿小矩形去拼 n by (m-n) 的矩形,這種類似貪婪算法的拼法並不能保證全局最優,這種方法在例子1和2中是可以的,但是例子3中就不適用了。由於沒有明確的策略去拼,為了得到全局最優解,只有遍歷所有情況,取其中的最小值。則第一個正方形可選的范圍就是 [1, n],對於其中任意一個值i來說,相當於左下角先放了個 i by i 的正方形,剩下的部分可以分為兩個矩形,有兩種不同的分法:水平切一刀的話,就分成了 (n-i) by m 的矩形和 i by (m-i) 的矩形;豎直切一刀的話,就分成了 (n-i) by i 的矩形和 n by (m-i) 的矩形,這兩種分法都要分別計算一下,參見簡陋的下圖所示:


         m
   --------------
   |n-i         |
 n |------------|
   | i |   m-i  |
   --------------

         m
   --------------
   |n-i|        |
 n |----        |
   | i |   m-i  |
   --------------

由於分割成的子矩形可以看作是一個子問題的重現,所以這道題用遞歸來做是非常合適的,同時為了避免大量的重復計算,應該使用記憶數組來保存計算過的值,其中 memo[i][j] 就表示 i by j 的矩形可以用最少的正方形拼出的個數。上面這種分割方法並不能包含所有的情況,比如例子3就無法通過這種方法得到。所以還有一種拼法,是同時在左下角和右上角各放一個正方形,然后再去拼剩余的部分,左下角的正方形邊長為i,范圍是 [1, n],右上角的正方形邊長為j,范圍是 [n-i+1, min(m-i, n)]。這里可能會有童鞋有疑問,為啥右上角的正方形邊長要從 n-i+1 開始,而不是從1開始呢?這是個好問題,因為這里需要 i+j 大於n,只有這樣才能區別於上面兩種拼法,否則的話,這種分割方法其實還是包括在前兩種分割方法里面的。

當這兩個邊角正方形大小確定了之后,剩余的部分可能需要分成三個矩形,就像例子3中所示一樣,中間還有個迷你矩形,其長寬需要特別計算一下,是個 (i+j-n) by (m-i-j) 的矩形,然后剩下的兩個矩形大小分別為 (n-i) by (m-j) 和 (n-j) by (m-i)。這些都理清了之后,代碼應該也就不難寫了。主要來看遞歸函數的寫法吧,首先判斷n和m的大小,若n大於m,則交換兩個參數。若n等於0,直接返回0,若n等於m,本身就是個正方形,返回1,若n等於1,則只能用 1 by 1 的正方形來拼,返回m,若 memo[n][m] 值大於0,說明當前情況已經計算過了,直接返回 memo[n][m]。否則開始正式計算,初始化結果 res 為整型最大值,然后遍歷左下角先拼的正方的邊長,之前說了,范圍是 [1, n],然后先計算分成兩個矩形的兩種情況,分別調用遞歸,並更新結果 res。然后就是計算右上角再放正方形的情況,其邊長范圍是 [n-i+1, min(m-i, n)],之前也分析過了,然后對分割出的三個小矩形分別調用遞歸,並用結果來更新 res 即可,參見代碼如下:


class Solution {
public:
    int tilingRectangle(int n, int m) {
        if (n > m) return tilingRectangle(m, n);
        vector<vector<int>> memo(n + 1, vector<int>(m + 1));
        return helper(n, m, memo);
    }
    int helper(int n, int m, vector<vector<int>>& memo) {
        if (n > m) return helper(m, n, memo);
        if (n == 0) return 0;
        if (n == m) return 1;
        if (n == 1) return m;
        if (memo[n][m] > 0) return memo[n][m];
        int res = INT_MAX;
        for (int i = 1; i <= n; ++i) {
            res = min(res, 1 + helper(n - i, m, memo) + helper(i, m - i, memo));
            res = min(res, 1 + helper(n, m - i, memo) + helper(n - i, i, memo));
            for (int j = n - i + 1; j < m - i && j < n; ++j) {
                res = min(res, 2 + helper(n - i, m - j, memo) + helper(i + j - n, m - i - j, memo) + helper(n - j, m - i, memo));
            }
        }
        return memo[n][m] = res;
    }
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/1240


參考資料:

https://leetcode.com/problems/tiling-a-rectangle-with-the-fewest-squares/

https://leetcode.com/problems/tiling-a-rectangle-with-the-fewest-squares/discuss/967635/Java-100-Recursion-with-Memoization.

https://leetcode.com/problems/tiling-a-rectangle-with-the-fewest-squares/discuss/791203/C%2B%2BDynamic-Programming-Backtracking-with-Figure.Explained.(100-faster)


LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM