LeetCode 1240. Tiling a Rectangle with the Fewest Squares

Posted by t3l on Sun, 27 Oct 2019 10:55:56 +0100

Let's start with a cheat program.

The first thought of the week was dp... Until you see Example 3, you can't use dp.. But looking at the boundary conditions of m and n, I feel that Example 3 should be the only special case, so I wrote dp directly, and finally I really Accepted it...
The idea is very simple, just paste the code directly

class Solution {
    public int tilingRectangle(int n, int m) {
        if(n<m){
            return tilingRectangle(m,n);
        }
        if(n==m){
            return 1;
        }
        if(n==13 && m==11){
            return 6;
        }
        
        int[][] dp = new int[14][14];
        for(int i=1;i<=n;i++){
            dp[i][i]=1;
            dp[1][i]=i;
            dp[i][1]=i;
        }
        
        for(int i=2;i<=n;i++){
            for(int j=i+1;j<=n;j++){
                dp[i][j]=i*j;
                
                for(int t=1;t<i;t++){
                    dp[i][j] = Math.min(dp[i][j],dp[t][j]+dp[i-t][j]);
                }
                
                for(int t=1;t<j;t++){
                    dp[i][j] = Math.min(dp[i][j],dp[i][t]+dp[i][j-t]);
                }
                dp[j][i]=dp[i][j];
            }
        }
        return dp[n][m];
    }
}

Pay attention to the special case processing code...

        if(n==13 && m==11){
            return 6;
        }

It's definitely wrong to do so. I saw other people's answers at the end of the week's competition and basically did so. Later, I saw a correct answer. It was done with dfs. Here we mainly analyze the code of dfs.

Correct method dfs

The reference link is here

The basic idea is to fill the whole square from the bottom to the top, first fill the bottom unfilled square each time, and choose a different possible size of square to fill it. We maintain a height array (skyline) in dfs. This skyline is a sign of state. The final result we want is that the skyline is the minimum number of squares in the state of n m. Of course, pure violence will have a high time complexity, but it can be pruned or optimized through the following three points.
1. If the current cnt of this skyline (that is, the number of blocks) has exceeded the current global optimal solution, then return directly.
2. The current skyline has been traversed, and the previous cnt is smaller than the current cnt, then return directly.
3. When we find the empty box in the lower left corner, we choose the next filled box from the larger box first, which can make the program converge quickly. (this is not pruning, but an important optimization)

The above is the explanation given by the original post, and the thinking is very detailed. I'm here to analyze the code in detail. See the code comments for details.

class Solution {
	// This set is a record, which is used in the following optimization [2].
    Map<Long, Integer> set = new HashMap<>();
    int res = Integer.MAX_VALUE;
    public int tilingRectangle(int n, int m) {
        if (n == m) return 1;
        if (n > m) {
            return tilingRectangle(m, n);
        }
        dfs(n, m, new int[n + 1], 0);
        return res;
    }
    
    private void dfs(int n, int m, int[] h, int cnt) {
    	// Optimizing [1]
        if (cnt >= res) return;
        boolean isFull = true;
        int pos = -1, minH = Integer.MAX_VALUE;
        
        // See if the array h is full. If not, record the lowest position minH and the corresponding index pos by the way.
        for (int i = 1; i <= n; i++) {
            if (h[i] < m) isFull = false;
            if (h[i] < minH) {
                pos = i;
                minH = h[i];
            }
        }

		// If it's full
        if (isFull) {
            res = Math.min(cnt, res);
            return;
        }
        
        // If it's not full, keep it.
        /* Here is a little trick, which maps the state of the whole array h to a long integer type.
        This mapping is one-to-one!
        The specific method is to treat the array as a (m+1) base number, which can be converted into a long type integer.
        Because the selection of base is (m+1), and each number in the array does not exceed m, it must be a one-to-one mapping!
        That is, it is impossible for two different arrays to map to an integer of the same long type.
        */
        long key = 0, base = m + 1;
        for (int i = 1; i <= n; i++) {
            key += h[i] * base;
            base *= m + 1;
        }
        if (set.containsKey(key) && set.get(key) <= cnt) return;
        set.put(key, cnt);
        
        // Find the end as low as pos
        int end = pos;
        while (end + 1 <= n && h[end + 1] == h[pos] && (end + 1 - pos + 1 + minH) <= m) end++;

		// After finding the lowest position of pos~end, start from end, traverse to pos position, and pave one brick at a time.
		// Continue with dfs
		// From the end, we can optimize [3].
        for (int j = end; j >= pos; j--) {
            int curH = j - pos + 1;
            
            int[] next  = Arrays.copyOf(h, n + 1);
            for (int k = pos; k <= j; k++) {
                next[k] += curH;
            }
            dfs(n, m, next, cnt + 1);
        }
    }
}

To sum up, the idea of dfs here is to find out the lowest interval, then traverse from the maximum length of the interval to 1, add bricks of corresponding length each time, and carry out the next dfs.