一、回溯算法
回溯算法,建议先看视频,b站搜索与回溯算法1
最经典的题就是全排列问题
从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。
如果m=n=3,期望输出:
1,2,3
1,3,2
2,1,3
2,3,1
3,1,2
3,2,1
那我们正常的思考其实就是先按顺序来,也就是1,2,3
。然后就返回到第二位换成3,接着第三位变成2,变成1,3,2
。然后1开头的搞完,换成2开头,操作和1一样。
上述操作重点要实现有以下两点:
- 能够每位数字不重复,那么说明需要有记忆功能
- 在第一输出
1,2,3
后,能够回到第二位,也就是前一位,变成之前没有使用过的数字,例如第二位第一次占据的是2,那么回到该位时,此时1被占用,2已经被占用过了,所以就安排3进行占位,输出1,3,2
。 - 对于一次全排列后的数值,要进行解封,因为下一次排列其实还是要使用的,不能一直设置为占用状态。
还是直接看代码吧,dfs的参数x代表索引,表示从哪个坐标开始进行全排列,一般设为0。n表示使用从1到n的n个数字进行全排列。以n=3为例,下面给出了详细的注解。也就是1,2,3 和1,3,2
的实现过程。如果还是看不懂,建议直接运行dfs(0,3);
debug一步一步的观察,就会明白了。
private static int[] a={0,0,0};//装载轨迹
private static int[] b=new int[4];//记录是否已经被使用,初始化为0,表示都还未使用过之后变为1,表示使用过,后来的数字不可以再用。多一位是因为从1到n,0其实是不使用的,为了之后不需要再为索引担心。举例下面索引是0-2,数值为1-3
public static void dfs(int x,int n){
if(x>=n){//进行满数组判断,即当已经进行过一次全排列,就进行返回,并输出此次全排列的结果
for(int q:a){
System.out.print(q);
}
System.out.println();
return ;//切记,必须要进行返回
}
for(int i=1;i<=n;i++){
if(b[i]==0) {//只有当前数字没有出现过,才能进行保存
a[x] = i;//将下标为x的数组设为i
b[i] = 1;//将i设为i已经使用
dfs(x+1,n);//进行下一个坐标的选值,再进入这个循环。例如第一次全排列,索引1知道1已经使用过,那就进行两次判断,设置为2;然后又进入该函数,进行当前的循环,此时进行三次判断,因为1,2都已经使用过了。此时再调入当前函数,x+1=n=3,所以执行x>=n中的方法,进行返回。然后执行↓
b[i]=0;这个是在函数返回时,执行这个代码,目的就是为了解放当前i=3,那么此时i=3,接着就要跳出整个循环,结束当前函数,也就是返回到之前调用该方法的地方,也就是索引1的位置进行调用的!此时的x=1.i=2,然后有执行该行代码,解放2,进行循环,变成i=3,因为3之前就解放过了,所以可以直接在x=1,也就是索引为1的位置填入3,然后将3进行占用,调用自身,进行x=2的填入,此时因为之前的3又进行了占用,2进行了解放,所以这里就只能填入2了,然后又进入x+1=n=3的方法体内,进行全排列组合1,3,2的输出。然后就再进行回溯…………
}
}
}
给出leecode上的一个题目,感兴趣的同学可以看一下。17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
给i出代码:
class Solution {
public List<String> letterCombinations(String digits) {
HashMap<Character,String> hm=new HashMap<Character,String>();
List<String> list=new ArrayList<String>();
StringBuilder stb=new StringBuilder();
hm.put('2',"abc");
hm.put('3',"def");
hm.put('4',"ghi");
hm.put('5',"jkl");
hm.put('6',"mno");
hm.put('7',"pqrs");
hm.put('8',"tuv");
hm.put('9',"wxyz");
char[] op=digits.toCharArray();
char[][] dd=new char[op.length][];
for(int uu=0;uu<op.length;uu++){
dd[uu]=hm.get(op[uu]).toCharArray();
}
if(digits != null && digits.length() != 0)
{
dfs1(dd,0,list,stb);
}
return list;
}
public static void dfs1(char[][] ss,int index,List<String> list,StringBuilder stb){
if(index>=ss.length){
list.add(stb.toString());
return;
}
for(char ab:ss[index]){
stb.append(ab);
dfs1(ss,index+1,list,stb);
stb.deleteCharAt(stb.length()-1);
}
}
}
回溯算法,其实也就是最后一层遍历完,返回到上一层,这个层数在这里也就是数字的位数。然后进行逐个遍历,因为是遍历char,所以一行中间不会有重复
回溯算法入口其实就是对于index,也就是回溯层数的设置,在回溯到最后一层,进行输出,之后返回该层,进行解封,然后继续遍历该层元素,直到第一层的元素也走到了最后。
2、背包问题
背包问题也是建议先看视频【动态规划】背包问题,讲解的很详细易懂。
题目描述 :
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
按照我们自己的思维是什么呢?
- 面对其中一件物品时,首先是装不装的下,装的下的东西装不装
- 装了会占用容量,会不会使之后更有价值的东西装不进来了呢?
那其实算法的实现也就是考虑了以上两点,其实主要是判断第二点,怎么判断我们是装的总价值大,还是不装的总价值大。好像还需要预测一下子的感觉,不太妙!!需要预测的原因在于我们对剩余容量和剩余物品的未知,那有什么办法是剩余容量大小正好是我们计算过的,知道最大价值的容量。也就是一个容量由小变大的过程,每一次判断我们都知道之前小容量下的最佳价值,那也就可以计算当前容量的装或者不装的最大价值。
建模过程看这里01背包问题 图解+详细解析 (转载),也就是将一个问题进行分解的过程,我们没办法预测,那就一步步往前推,每一步都保证最优,那到最后也就是最优的状态。
对于算法的核心进行建模,Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,V(i,j)表示当前背包容量 j,前 i 个物品最佳组合对应的价值,那么判断当前物品的最佳价值的过程如下:
1、是否装得下:装不下,V(i,j)=V(i-1,j);装得下,那装不装?
2、进行装的价值和不装的价值进行比较。不装的话,就如上;装的话,那就是当前物品的价值加上剩余容量的最大价值。剩余容量的最大价值我们是知道的,因为容量是一点点增大的。
装的价值:V(i,j-w(i))+v(i) 剩余容量的最大价值加上当前物品的价值
不装的价值:V(i-1,j)
最后比较上面两个式子的大小即可
看一道leecode上的题474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。(注意是所有元素的0,1加起来不超过,不是单个元素)
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例一
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例二
输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。
这道题可以用背包问题来做,也就是将物品换成数组里的字符,因为有0,1两个限制,所以我设置成了三维数组。注意三点:
1、背包原理。因为有两个变量,所以要建三维数组
2、注意 f[i][o][u]=f[i-1][o][u];作为切入口,选择前0个时最大子集个数为0;
3、注意char字符的运算操作,字符加减是先转化为int加减后,返回字符的。
public class day7 {
public static int findMaxForm(String[] strs, int m, int n) {
int[][][] f = new int[strs.length+1][m+1][n+1];// 备忘录
int mmax=0;
for(int i=1;i<=strs.length;i++){
for(int o=0;o<=m;o++){
for(int u=0;u<=n;u++){
f[i][o][u]=f[i-1][o][u];
int[] oop=day7.countZeroAndOne(strs[i-1]);
if(oop[0]<=o&&oop[1]<=u){
f[i][o][u]=Math.max(f[i][o][u],f[i-1][o-oop[0]][u-oop[1]]+1);
}
if(f[i][o][u]>mmax){
mmax=f[i][o][u];
}
}
}
}
return mmax;
}
private static int[] countZeroAndOne(String str) {
int[] cnt = new int[2];
for (char c : str.toCharArray()) {
cnt[c - '0']++;
}
return cnt;
}
}
补充:背包原理就相当于把问题转化为放物品的问题,然后在这个放物品的过程中,每次放物品现实背包体积一定的情况下,物品逐渐增多,然后再让背包体积逐渐增大,在原来的所选物品上再进行增加物品,这个时候是很好筛选的,因为我们已经知道如果不放的话,我们的价值为多少和放的话就是背包容积变小后的价值加上现在物品的价值,我们每次都选最大的就好。一直将背包体积增加到边界返回的即是最大值。