遇到求最值的算法问题,首先要思考的就是:如何穷举出所有可能的结果?
穷举主要有两种算法,就是回溯算法和动态规划,前者就是暴力穷举,而后者是根据状态转移方程推导「状态」。
如何将戳气球问题转化成回溯算法呢?其实就是想穷举戳气球的顺序,不同的戳气球顺序可能得到不同的分数,把所有可能的分数中最高的那个找出来。
回溯算法
1 | int res = Integer.MIN_VALUE; |
动态规划
1 | int maxCoins(int[] nums) { |
读了一些书,看了一些文章,编过一些小例程,搞了一些没有什么技术含量的开发工作。
遇到求最值的算法问题,首先要思考的就是:如何穷举出所有可能的结果?
穷举主要有两种算法,就是回溯算法和动态规划,前者就是暴力穷举,而后者是根据状态转移方程推导「状态」。
如何将戳气球问题转化成回溯算法呢?其实就是想穷举戳气球的顺序,不同的戳气球顺序可能得到不同的分数,把所有可能的分数中最高的那个找出来。
回溯算法
1 | int res = Integer.MIN_VALUE; |
动态规划
1 | int maxCoins(int[] nums) { |
解决两个字符串的动态规划问题,一般都是用两个指针 i,j
分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。
1.递归
base case 是 i
走完 s1
或 j
走完 s2
,可以直接返回另一个字符串剩下的长度。
对于每对字符 s1[i]
和 s2[j]
,可以有四种操作:
1 | if s1[i] == s2[j]: |
⏬⏬⏬
1 | public int minDistance(String word1, String word2) { |
这段代码有点小问题就是,这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。
对于子问题 dp(i-1, j-1)
,如何通过原问题 dp(i, j)
得到呢?有不止一条路径,比如 dp(i, j) -> #1
和 dp(i, j) -> #2 -> #3
。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。
做动态规划问题时,肯定会对dp
数组的遍历顺序有些头疼,拿二维dp
数组来举例
有时候是正向遍历:
1 | int[][] dp = new int[m][n]; |
有时候反向遍历:
1 | for (int i = m - 1; i >= 0; i--) |
有时候可能会斜向遍历:
1 | // 斜着遍历数组 |
「最优子结构」是某些问题的一种特定性质,并不是动态规划问题专有的。也就是说,很多问题其实都具有最优子结构,只是其中大部分不具有重叠子问题,所以我们不把它们归为动态规划系列问题而已。
先举个很容易理解的例子:假设学校有 10 个班,已经计算出了每个班的最高考试成绩。那么现在要求计算全校最高的成绩,会不会算?当然会,而且不用重新遍历全校学生的分数进行比较,而是只要在这 10 个最高成绩中取最大的就是全校的最高成绩。
提出的这个问题就符合最优子结构:可以从子问题的最优结果推出更大规模问题的最优结果。算每个班的最优成绩就是子问题,知道所有子问题的答案后,就可以借此推出全校学生的最优成绩这个规模更大的问题的答案。
所以看,这么简单的问题都有最优子结构性质,只是因为显然没有重叠子问题,所以我们简单地求最值肯定用不出动态规划。
1、输入一个字符串,可以包含
+ - * / ()
、数字、空格,你的算法返回运算结果。2、要符合运算法则,括号的优先级最高,先乘除后加减。
3、除号是整数除法,无论正负都向 0 取整(5/2=2,-5/2=-2)。
4、可以假定输入的算式一定合法,且计算过程不会出现整型溢出,不会出现除数为 0 的意外情况。
比如输入如下字符串,算法会返回 9:
1
2 >3 * (2-6 /(3 -7))
>
可以看到,这就已经非常接近我们实际生活中使用的计算器了,虽然我们以前肯定都用过计算器,但是如果简单思考一下其算法实现,就会大惊失色:
1、按照常理处理括号,要先计算最内层的括号,然后向外慢慢化简。这个过程我们手算都容易出错,何况写成算法呢!
2、要做到先乘除,后加减,这一点教会小朋友还不算难,但教给计算机恐怕有点困难。
3、要处理空格。我们为了美观,习惯性在数字和运算符之间打个空格,但是计算之中得想办法忽略这些空格。
1 | static int i = 0; |
这个问题转化为背包问题的描述形式:
有一个背包,最大容量为
amount
,有一系列物品coins
,每个物品的重量为coins[i]
,每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?若只使用
coins
中的前i
个硬币的面值,若想凑出金额j
,有dp[i][j]
种凑法。如果你不把这第
i
个物品装入背包,也就是说你不使用coins[i]
这个面值的硬币,那么凑出面额j
的方法数dp[i][j]
应该等于dp[i-1][j]
,继承之前的结果。如果你把这第
i
个物品装入了背包,也就是说你使用coins[i]
这个面值的硬币,那么dp[i][j]
应该等于dp[i][j-coins[i-1]]
。
1 | public int change(int amount, int[] coins) { |
背包问题大致的描述是什么:
给你一个可装载重量为
W
的背包和N
个物品,每个物品有重量和价值两个属性。其中第i
个物品的重量为wt[i]
,价值为val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?那么对于这个问题,我们可以先对集合求和,得出
sum
,把问题转化为背包问题:给一个可装载重量为
sum / 2
的背包和N
个物品,每个物品的重量为**nums[i]
。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
1 | public boolean canPartition(int[] nums) { |
1 | // 当前状态为 (K 个鸡蛋,N 层楼) |
如果鸡蛋碎了,那么鸡蛋的个数K应该减一,搜索的楼层区间应该从[1..N]变为[1..i-1]共i-1层楼
如果鸡蛋没碎,那么鸡蛋的个数K不变,搜索的楼层区间应该从 [1..N]变为[i+1..N]共N-i层楼
最坏情况下扔鸡蛋的次数,所以鸡蛋在第i层楼碎没碎,取决于哪种情况的结果更大
🔽🔽🔽
1 | Map<String, Integer> map = new HashMap<>(); |
结果超时了,可以将for循环改成二分搜索
1 | Map<String, Integer> map = new HashMap<>(); |
1 | /** |
tips:
调用前先给数组排序
1 | nsum调用 |