从 LeetCode 279 实践动态规划

Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, ...) which sum to n.

For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.

原题如上,乍一看觉得很简单,但是仔细一想,自己举了几个例子,比如 n = 147, 则 n = 7² + 7² + 7²,或者 n = 11² + 5² + 1 ,而不是 12²+1+1+1。所以这个问题还是需要对平方值小于 n 的每一个值 i 进行考虑,即遍历。而剩下的对于每一个 n-i² 又是一个不确定的数,于是很自然地就想到递归,对于每一个 n-i² 不就是和 n 一样的问题吗,只不过是数字不同罢了。因此将求n的 least number of perfect square numbers 问题转化为多个 n-i² 子问题。

对于将母问题转化为多个子问题的算法,目前我学到的主要有分治算法和动态规划算法。这两种算法都是由数学上的递推公式而来。那么区别在哪里呢?

一般来说,分治算法会使用递归,将母问题转化为几个(2个以上)子问题解的和,也就是说子问题的解是组成母问题解的一部分。而动态规划算法一般将递归算法写成非递归算法(将子问题的解记录在一个表中来实现),通过对子问题解的筛选,来决定母问题的解。本题显然是后者,因此采用动态规划算法更为合适。

写这篇文章之前看了一个博主关于动态规划算法的介绍,感觉非常有趣,很适合新手入门,链接如下:

很特别的一个动态规划入门教程_动态规划想不出来应该怎么办-CSDN博客

确定一个问题能否用动态规划算法,需要确定如下几个关键点:

  • 最优子结构

对于 i² 小于 n 的值,必然有一个是最优解(不排除多个最优解,但我们只需要找到一个即可)。

  • 子问题重叠

求解每一个子问题 n-i² 的 least number of perfect square numbers 和求解母问题 n 的 least number of perfect square numbers ,是一样的问题,只不过数值大小不同罢了。

  • 子问题独立

求解一个子问题 n-i² 并不会影响另一个子问题的解。

  • 边界

最后一个子问题一定是所求的数值可以由一个平方数直接表示。

  • 备忘录

大家应该都知道递归之所以慢,很大程度上是因为进行了很多不必要的重复计算。为了保证对每一个子问题只

进行一次求解,需要使用一个表来记录所有计算过的子问题的解。

以下是使用了递推的动态规划代码实现。虽然保证每个子问题只计算了一遍,但由于使用了递归等原因,因此

效率上还是有待改善。

public class Solution {
    public int numSquares(int n) {
        if(n<=0)
            return 0;
        int record[] = new int[n+1]; //⑤备忘录,保证计算过的n的结果result再次需要时可以直接使用
        for(int i=0;i<n+1;i++)
            record[i] = 0;
        return result(n,record);
    }
    public int result(int n,int record[]){ //递归,从结果向起始推    非递归,从起始向结果推
        int k = Squre(n);
        if(n == k*k) //③边界
            return 1;
        int time = Integer.MAX_VALUE;
        for(int i = 1;i<=k;i++){ //④子问题独立
            if(record[n-i*i] == 0)
                record[n-i*i] = result(n-i*i,record); //②子问题重叠,子问题和母问题是同样的问题
            time = Math.min(time,record[n-i*i]+1); //①最优子问题
        }
        return time;
    }
    public int Squre(int n){
        if(n<=0)
            return 0;
        int i = 1;
        while(i*i<=n)
            i++;
        return i-1;
    }
}

上述使用递归的动态规划算法是从结果向起始推进的。有一种效率更高的方案,不使用递归,从起始向结果推进。

二者的思考方向相反,可以借鉴一下。

dp[n] indicates that the perfect squares count of the given n, and we have:

dp[0] = 0 
dp[1] = dp[0]+1 = 1
dp[2] = dp[1]+1 = 2
dp[3] = dp[2]+1 = 3
dp[4] = Min{ dp[4-1*1]+1, dp[4-2*2]+1 } 
      = Min{ dp[3]+1, dp[0]+1 } 
      = 1				
dp[5] = Min{ dp[5-1*1]+1, dp[5-2*2]+1 } 
      = Min{ dp[4]+1, dp[1]+1 } 
      = 2
						.
						.
						.
dp[13] = Min{ dp[13-1*1]+1, dp[13-2*2]+1, dp[13-3*3]+1 } 
       = Min{ dp[12]+1, dp[9]+1, dp[4]+1 } 
       = 2
						.
						.
						.
dp[n] = Min{ dp[n - i*i] + 1 },  n - i*i >=0 && i >= 1

and the sample code is like below:

public int numSquares(int n) {
	int[] dp = new int[n + 1];
	Arrays.fill(dp, Integer.MAX_VALUE);
	dp[0] = 0;
	for(int i = 1; i <= n; ++i) {
		int min = Integer.MAX_VALUE;
		int j = 1;
		while(i - j*j >= 0) {
			min = Math.min(min, dp[i - j*j] + 1); //从起始向结果考虑,计算从dp[1]开始到dp[n]
			++j;
		}
		dp[i] = min;
	}		
	return dp[n];
}



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值