动态规划是是我目前觉得遇见的最难的题目了。说起来到处都有他,但是真正用的时候,如果变个形状,又很难想到动态方程。所以,目前的动态规划,还是要多总结,找到思路。
抛砖迎玉,先来说个简单的:上台阶问题。现在有一层楼梯,共n层台阶。每次上台阶,可以上1层或者2层,问一共有多少层走完楼梯的方案。
这个题应该都很熟悉,很明显这个是斐波那契数列。怎么推算出来,这是个斐波那契数列的呢?
假设我站在第k层台阶上面,要到达第k层台阶,可以由第k-1层台阶,走一步,和第k-2层台阶,走两步,到达第k层。
在思考的过程中,可能最搞不明白的就是,为什么不考虑第k-3层,或者其他k-n层的台阶,走3层,或者走4层来到达第k层。(最长回文子串也会这样想,考虑k的时候,是否考虑string[i-1]和string[j-1])。
原因是因为第k-1层台阶,和第k-2层台阶,已经考虑到了k-3层,和k-4层。(其中,k-1层考虑到了k-2和k-3层,原因就在于只能走1步和2步,但是k-1考虑到的k-2只是从 k-2 走 一步 到k-1这一情况)
这种状态的转移很饶人,就跟刚学一个知识点一样,很不清楚。习惯就好了,想真正熟悉,估计只有多用,不断重复了。
下面说一下这个题目的进阶,现在我能走的不是只有1层或2层,而是能走a[1],a[2]…a[n]层,怎么说?难道a[k] = a[1]+a[2]+…+a[k-1];没错,就是这样!
for (int i = 0; i < n; i++) {
int ans = 0;
for (int j = a[i]; j <= k; j++) {
ans = ans + dp[j - a[i]];
}
dp[i] = ans;
}
return dp[k];
从这里面应该就能明白,为什么说菲波拉契数列是最简单的动态规划了。
ok,先放下这个题,来看看另一个题,就是动态规划的经典——背包问题(关于背包可以看之前的博客总结笔试中背包问题的应用)——先来看看多重背包的场景:被告容量为V,一共有n中物品,每个价值为a[i],体积为b[i],问背包最大的能装多少(物品可以装多次)。动态转移方程如下:
for(int i=0;i<n;i++){
for(int j=a[i];j<=V;j++){
dp[j] = Math.max(dp[j],d[j-b[i]+a[i]]);
}
}
return dp[V];
注意,这里每个物品可以装多次。为什么不采用贪心?贪心采用性价比最高的,从大到小排,从大往小装(当然不对了,这样装,无法保证全局最优)。
如果是递归呢?每个背包选或者不选,有2^n情况,显然复杂度太大了。
上面的背包得到的的是能装的最大价值,而无法拿到达到最大价值有多少种方案。
那么,要求出多少方案怎么做呢?
是不是可以这样想,把走台阶看成背包容量,这样,状态方程就和上面走台阶一样了。
来个例子:
有两种硬币,第一种可以无限使用,第二种每个只能使用一次.。第一种硬币为n个,价值分别任a[0],a[1],a[2]…a[n-1],第二种硬币为m个,价值分别为b[0],b[1]…b[n-1],求组合成价值为k的硬币数量
for(int i=0;i<n;i++){
for(int j=a[i];j<=k;j++{
dp1[j] = dp1[j]+dp1[j-a[i]];
}
}
for(int i=0;i<m;i++){
for(int j=k;j>=a[i];j--){
dp2[j] = dp2[j-a[i]]+dp2[j];
}
}
for(int i=1;i<=k;i++){
ans[k] += dp1[i]*dp[k-i];
}
return ans[k];
如果要求打印所有路径呢?这就必须要用到dfs了。
还是来给个例题。leetcode上面
单词接龙 II
这个题,是单词接龙 I的变形。
这个题目其实与动态规划可以说有关,也可以说是无关(为什么,等会说明),也可以当做一个搜索题,放在这里是为了把相似的题目放在一起,便于总结。
首先,需要基于单词接龙 I得到最短的路径的单词个数(两种方法,1.bfs。2.动态规划),其次,需要基于搜索(宽搜或者广搜都可以,网上面的教程大多是基于广搜,所以楼主等会给一下宽搜的代码)。
这里暂时先来讨论如何得到最短路径的单词数。宽搜就不说了。那么动态规划怎么做呢?
不知道大家时候还记得最短路算法。最短路算法中,有一个很有代表性的算法,bellman_ford算法(dijkstra基于贪心),其中,基于bellman_ford算法的优化版本,也可以说是bellman_ford和dijkstra的结合版本,SPFA(中国人发明的,挺牛的),就可以运用到这个题目中。
怎么看成是最短路呢?题目要求,只能相差一个字母,所以,可以把,相差一个字母的两个字符串之间的距离,看做1,相差不是1的,可以把其看成是无限远,然后利用这个1去更新其他的距离。
具体代码就不给了,这个不难,只要理解了SPFA的概念,这个题目的答案就很好写出来。
下面再来看 单词接龙 II 的解法。
这个网上大部分都是基于dfs,楼主这里给出bfs。需要说明的是,这个题目有点卡数据。采用bfs时,如果从beginWord开始搜索,会超时,但是,从endWord开始搜索,就可以通过,注意这一点就行了。
一下是AC代码:
class T {
int x;//记录这个单词的位置
String ts;//记录单词
List<String> list;//记录走到当前单词,路径上的所有单词
public T(int x, String ts, List<String> list) {
this.x = x;
this.ts = ts;
this.list = list;
}
}
/**
* 记录下单词是否只差一个,注意,必须差一个,两个单词完全相同也不可以
* 如果两个单词完全相同,相当于重复了,当做一个单词算
*/
boolean isT(String ts, String ts2) {
int s = 0;
for (int i = 0; i < ts.length(); i++) {
if (ts.charAt(i) != ts2.charAt(i)) {
s++;
}
if (s > 1) {
return false;
}
}
return s == 1;
}
class Arr {//记录邻接矩阵(即Arr[i].slist代表与第i个单词相差一个字母的所有单词)
List<String> slist;//
List<Integer> ilist;//对应单词的相对位置
public Arr(List<String> slist, List<Integer> ilist) {
this.slist = slist;
this.ilist = ilist;
}
}
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
if (beginWord == null || beginWord.length() == 0 || endWord == null || endWord.length() == 0)
return null;
int len = wordList.size();
Arr[] listArrArr = new Arr[len + 1];
//构建动态表Arr[len+1]
for (int i = 0; i < len; i++) {
listArrArr[i] = new Arr(new ArrayList<String>(), new ArrayList<Integer>());
for (int j = 0; j < len; j++) {
if (j != i && isT(wordList.get(i), wordList.get(j))) {
listArrArr[i].slist.add(wordList.get(j));
listArrArr[i].ilist.add(j);
}
}
}
//把beginWord,当做第len个单词
listArrArr[len] = new Arr(new ArrayList<String>(), new ArrayList<Integer>());
for (int i = 0; i < len; i++) {
if (isT(beginWord, wordList.get(i))) {
listArrArr[len].slist.add(wordList.get(i));
listArrArr[len].ilist.add(i);
}
}
//这个单词二维list,记录没有过前的所有答案()
List<List<String>> preList = new ArrayList<>();
int min = Integer.MAX_VALUE;//记录最短路径的单词数
//可能这里有疑问,既然已经记录了最短路径单词数,为什么还要过滤preList呢
//原因在于,可能到达最短路径的时候,"下一层"的单词已经放如队列中了
Queue<T> queue = new LinkedList<>();
ArrayList<String> tsi = new ArrayList<>();
tsi.add(endWord);
int anspos = wordList.indexOf(endWord);//记录endWord出现的位置
if (anspos != -1)//说明能达到beginWord
queue.add(new T(anspos, endWord, tsi));
Set<String> set = new HashSet<>();//这里Set,用来记录所有已经走过的路
/*
举个例子,比如答案是
[tad ted red rex],[tad,red,
*/
boolean isGetAns = false;
while (!queue.isEmpty()) {
T t = queue.poll();
set.add(t.ts);//注意,只有到了这里,才能加入set。以后如果还能通过这个t.ts变换,也不是最优解,
// 所以通过这里,让以后不能再使用这个单词。
if (listArrArr[len].ilist.contains(t.x)) {//最终答案邻接表是否包含,包含说明是一个解。
t.list.add(beginWord);
min = Math.min(t.list.size(), min);
if (!preList.contains(t.list))//可能有重复的,重复的去掉
preList.add(new ArrayList<>(t.list));
isGetAns = true;//得到最优解之后,不再往queue里面添加任何东西。
continue;
}
if (!isGetAns) {
for (int i = 0; i < listArrArr[t.x].slist.size(); i++) {
//这是其中很大的一个优化,就是把能到达的单词预存到这个数组中。而不是采用遍历所有的数组的方式
if (!set.contains(listArrArr[t.x].slist.get(i))) {
List<String> sArr = new ArrayList<>(t.list);
sArr.add(listArrArr[t.x].slist.get(i));
//set.add(listArrArr[t.x].slist.get(i));不能在这里添加入set
queue.add(new T(listArrArr[t.x].ilist.get(i), listArrArr[t.x].slist.get(i), sArr));
}
}
}
}
//由于是从后往前搜索,所以最后顺序需要倒过来
List<List<String>> ansLL = new ArrayList<>();
for (int i = 0; i < preList.size(); i++) {
if (preList.get(i).size() == min) {
ArrayList<String> tsList = new ArrayList<>();
int jlen = preList.get(i).size();
for (int j = 0; j < preList.get(i).size(); j++) {
tsList.add(preList.get(i).get(jlen - j - 1));
}
ansLL.add(tsList);
}
}
return ansLL;
}