目录
NC20 数字字符串转化成IP地址
描述
现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。
例如:给出的字符串为"25525522135",返回["255.255.22.135", "255.255.221.35"]. (顺序没有关系)
数据范围:字符串长度 0≤𝑛≤120≤n≤12,注意:ip地址是由四段数字组成的数字序列,格式如 "x.x.x.x",其中 x 的范围应当是 [0,255]。
题目链接: https://www.nowcoder.com/practice/ce73540d47374dbe85b3125f57727e1e
输入:"25525522135" ,返回值:["255.255.22.135","255.255.221.35"]
回溯模板
1,确定初始回溯索引位置,回溯过程中的中间状态。本题中 dfs(s,0,"") s 是不变的,我们要搜索它得到整个解,0是索引初始位置,"" 是初始中间状态。
2,确定 dfs() 的结束状态,比如回溯起始位置到达问题 arr 数组或字符串的末尾,或是中间状态符合期望的结果。本题中是找到一个合法的 ip 字符串,如果发现中间状态 tmp 不是一个合法的ip,就终止本次回溯。
3,确定回溯中起始位置,本题中可以从 2.xxx,25.xxx,255.xxx,2552.xxx ... 开始回溯。回溯函数里一般都会有一层for 循序,每次回溯都会用 for 遍历选择某个元素或不选择某个元素并继续递归地回溯。每次回溯的起始位置可以是问题 arr数组或字符串索引 idx 中的任一位置,可以是0,也可以是上次回溯起始位置加 1 ,0 =< idx < s.length-1 。
4,处理回溯逻辑,确定下一次回溯的条件。比如要不要选择某个元素,要不要在某个位置添加一个"."点号,该位置要回溯几次等等......比如获取所有子集获取全排列,根据我们的需求来确定怎么进行下次回溯。
本题中是找到一个从回溯起始位置截断的子串,如果中间状态字符串 tmp 不为空就向后添加 "." 并添加这个子串,并把回溯起始位置后置1 。
public static class Solution4 {
static ArrayList<String> res = new ArrayList<String>();
public static List<String> restoreIpAddresses(String s) {
// s 长度不能超过 3*4
if (s.length() > 12) {
return res;
}
// 回溯算法,和深度优先差不多
// 0 是开始回溯索引位置,"" 是字符串
dfs(s, 0, "");
return res;
}
// 回溯函数,每次回溯都会进入一层 for 循环确定要不要在某元素后添加一个点号
private static void dfs(String s, int idx, String tmp) {
// 剪枝
if (tmp.split("\\.").length > 4) {
return;
}
// 得到一个合法的结果
if (idx >= s.length()) {
res.add(tmp);
return;
}
// 确定起始位置,比如 25525522135 从 2.xxx开始,
// 也可以是从 25.xxx 或 255.xxx ... 开始去回溯
for (int i = idx; i < s.length(); i++) {
String str = s.substring(idx, i + 1);
if (isLegal(str)) {
// 只要 tmp 不为空就添加 “.” 号作为截断
// 该做法不论之后是否得到的 tmp 是不是合法 ip 都加快了回溯速度
if (!tmp.isEmpty()) {
dfs(s, i + 1, tmp + "." + str);
} else {
// 开头只进来一次
// 如 2.xxx,25.xxx,255.xxx 都会进入一次
dfs(s, i + 1, str);
}
}
}
}
// 确定 str 是否在 0 到 255 内且不以 0 开始
private static boolean isLegal(String str) {
if (str.length() > 3) {
return false;
}
if (str.length() == 3 && str.charAt(0) == '0') {
return false;
}
if (str.length() == 2 && str.charAt(0) == '0') {
return false;
}
int num = Integer.parseInt(str);
if (num <= 255 && num >= 0) {
return true;
}
return false;
}
}
NC27 集合的所有子集(一)
一个和它类似的题目,现在有一个没有重复元素的整数集合S,求S的所有子集
public static class Solution5 {
public static void main(String[] args) {
int[] ints = {1, 2, 3};
System.out.println(subsets(ints));
}
public static ArrayList<ArrayList<Integer>> res = new ArrayList<>();
public static ArrayList<ArrayList<Integer>> subsets(int[] arr) {
// 0 是回溯起始位置,tmp 是中间结果
dfs(arr, 0, new ArrayList<Integer>());
// 排序
Collections.sort(res, new Comparator<ArrayList<Integer>>() {
@Override
public int compare(ArrayList<Integer> o1, ArrayList<Integer> o2) {
int o1Size = o1.size();
int o2Size = o2.size();
if (o1Size != o2Size) return Integer.compare(o1Size, o2Size);
else {
for (int i = 0; i < o1.size(); i++) {
int comp = Integer.compare(o1.get(i), o2.get(i));
if (comp != 0) return comp;
}
}
return 0;
}
});
return res;
}
// 回溯函数,每次进入回溯时都要进入 for 循环遍历 arr 数组选择是否选取某元素进行下次遍历
public static void dfs(int[] arr, int idx, ArrayList<Integer> tmp) {
// 任意结果都可接受
Collections.sort(tmp);
if (!res.contains(tmp)) {
res.add(new ArrayList<>(tmp));
}
// 回溯终止条件
if (idx >= arr.length) {
return;
}
// 每次回溯过程中回溯起始元素位置之后的元素都有被选择的可能,可以选,也可以不选
for (int i = idx; i < arr.length; i++) {
// 每个回溯起始位置的元素都不要,即不选
dfs(arr, i + 1, new ArrayList<>(tmp));
// 每个回溯起始位置的元素都要
tmp.add(arr[i]);
// 本次元素arr[idx]回溯结束后还要进入for循环,对下一个起始元素进行回溯
dfs(arr, i + 1, new ArrayList<>(tmp));
}
}
没有从 0 开始回溯的例子:
BM55 没有重复项数字的全排列
假如 还是套用上面的模板,从索引 0 处开始回溯,就得不到正确结果。因为每个元素都要用到 1 次,但顺序不变。
public static class Solution8 {
public static void main(String[] args) {
System.out.println(permute(new int[]{1, 2, 3}));
}
public static ArrayList<ArrayList<Integer>> res = new ArrayList<>();
public static ArrayList<ArrayList<Integer>> permute(int[] num) {
dfs(num, new ArrayList<Integer>());
return res;
}
private static void dfs(int[] num, ArrayList<Integer> tmp) {
// 回溯结束条件
if (tmp.size() == num.length) {
res.add(new ArrayList<>(tmp));
return;
}
// 每次进入递归函数时每个元素都有被选择的可能,但已经选过的就跳过
for (int i = 0; i < num.length; i++) {
if (!tmp.contains(num[i])) {
// 选择本元素后进入递归,递归完后再进入for循环
tmp.add(num[i]);
dfs(num, tmp);
// 相当于此次进入递归函数没选择本元素,下次进入递归函数时可以再选,
// 执行完这条语句后进入for循环遍历其他未选中的元素
tmp.remove(tmp.size() - 1);
}
}
}
}
BM56 有重复项数字的全排列
public class Solution9 {
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
public ArrayList<ArrayList<Integer>> permuteUnique(int[] num) {
Arrays.sort(num);
// 因为每个元素都必须使用一次
boolean[] visited = new boolean[num.length];
dfs(num, new ArrayList<>(), visited);
// 排序
res.sort(new Comparator<ArrayList<Integer>>() {
@Override
public int compare(ArrayList<Integer> o1, ArrayList<Integer> o2) {
for (int i = 0; i < o1.size(); i++) {
if (o1.get(i) < o2.get(i)) {
return 1;
} else if (o1.get(i) > o2.get(i)) {
return -1;
}
}
return 0;
}
});
return res;
}
// 回溯函数里继续递归回溯,每次回溯都会进入一层for循环,即每次回溯都只是确定该某一个位置的元素是否选择
private void dfs(int[] num, ArrayList<Integer> tmp, boolean[] visited) {
if (tmp.size() == num.length) {
res.add(new ArrayList<>(tmp));
}
// 每次回溯每个元素都有被选中的可能
for (int i = 0; i < num.length; i++) {
// 剪枝,如果前一个相同的元素A选择不访问,那我们也不访问,因为A后面必定会被
// 访问
if((i>0)&&(num[i]==num[i-1])&&(!visited[i-1])){
continue;
}
// 每次回溯只选择未被选择的元素
if (!visited[i]) {
// 本次回溯选择了本元素
tmp.add(num[i]);
visited[i] = true;
// 本次选择已结束,进入下次回溯,下次回溯又会进入for循环,每个元素都有被选择的可能
dfs(num, tmp, visited);
// 恢复原状,相当于本次for遍历 没有选择本元素,后面应该要遍历下一个元素
tmp.remove(tmp.size() - 1);
visited[i] = false;
}
}
}
}
最后
回溯算法也可以用在一些 n*m 的矩阵里面,这时候就不是在一个回溯函数里套一个 for 循环这样了,但回溯的思路是不变的,因为矩阵不是只有一个 0 到 n 的单一搜索方向,它每次都有东南西北四个方向要分别进行深度优先搜索,每个矩阵元素 matrix[i][j] 都可以作为深度搜索的起点并进行四个方向的搜索遍历。