极简网络
mengxh1990 / wisenetwork Goto Github PK
View Code? Open in Web Editor NEW极简网络
极简网络
leetcode定义
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
回溯算法的基本**是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
与递归,DFS关系
回溯算法是个**,回溯法的实现方法有两种:递归和递推(也称迭代,非递归)。一般来说,一个问题两种方法都可以实现,只是在算法效率和设计复杂度上有区别。
与DFS的整体过程是一致的,都是一条路走到底,不撞南墙不回头。回溯算法是枚举,枚举的方式是深度优先搜索,回溯算法需要在深度优先搜索的同时,把临时路径一直传递下去。
回溯算法三元素
这类问题的解可表示成一个n元组(x0,x1,…,xn-1),求满足约束条件的可行解,或进一步求使目标函数取最大(或最小)值的最优解问题,这类问题一般都可以用回溯法求解。
回溯法通过使用约束函数和限界函数来压缩需要实际生成的状态空间树的结点数,从而大大节省问题求解时间。
解空间树的三种节点
确定了解空间的组织结构后,回溯法从根节点出发,以深度优先的方式搜索整个解空间。这个开始节点成为一个活节点,同时也成为当前的扩展节点。在当前的扩展节点处,搜索向纵深方向移至一个新节点。这个新节点就成为一个新的活节点,并成为当前扩展节点。如果在当前扩展节点处不能再向纵深方向移动,则当前的扩展节点就成为死节点。此时,应往回移动即回溯至最近的一个活节点处,并使这个活节点成为当前的扩展节点。回溯法即以这种方式递归地在解空间中搜索,直到找到所要求的解或者解空间中已无活节点为止。
以0/1背包问题为例
public class Knapsack {
private int curWeight;
private int curValue;
private int bestValue;
private final int CAPACTITY = 25;
private final int NUM = 3;
private int[] bestResult = new int[NUM];
public void knapsack(int[] weights, int[] values) {
//初始时从0开始搜索
backtrack(weights, values, 0, new int[NUM]);
for (int i = 0; i < NUM; i++) {
System.out.println(bestResult[i]);
}
System.out.println(bestValue);
}
private void backtrack(int[] weights, int[] values, int index, int[] path) {
if (index > NUM - 1) {//到达叶子节点,得到一个可行解
if (curValue > bestValue) {//判断是否为最优解
bestValue = curValue;//更新最优解
for (int i = 0; i < NUM; i++) {
bestResult[i] = path[i];
}
}
return;
}
//1,确定每一步的选择集,0/1背包问题每步的选择只有0和1
for (int i = 0; i <= 1; i++) {
//2,更新临时路径
path[index] = i;
//3,如果选择0,则直接向前扩展
//4,如果选择1,并且没有超过容量,则可以继续扩展,否则剪枝
if (curWeight + weights[index] * i <= CAPACTITY) {
//4.1 更新当前值(做选择)
curValue += values[index] * i;
curWeight += weights[index] * i;
//4.2 纵深扩展
backtrack(weights, values, index + 1, path);
//4.3 恢复当前值(撤销选择),以便回溯
curValue -= values[index] * i;
curWeight -= weights[index] * i;
}
}
}
}
一般用递归实现,递归函数设计:
1、必须要有一个临时变量(初始时可以就直接传递一个字面量或者常量进去)传递不完整的解,表示截至当前解的情况,因为每一步选择后,暂时还没构成完整的解,这个时候这个选择的不完整解,也要想办法传递给递归函数。也就是,把每次递归的不同情况传递给递归调用的函数。【每一步的解的汇总信息传递下去】表示临时路径。
2、可以有一个全局变量,用来存储完整的每个解,一般是个集合容器(也不一定要有这样一个变量,因为每次符合结束条件,不完整解就是完整解了,直接打印即可)。【存储完整的解空间,也可以用类的属性】
3、最重要的一点,一定要在参数设计中,可以得到结束条件。一个选择是可以传递一个递增变量,也许是数组的长度,也许是数量,索引等等。【结束条件】
4、要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。【恢复状态:下一次递归前,把当前这一步的选择清除或者还原】
回溯的意思就是要回去,递归函数自动保证了回去,但是我们设置的其他变量如果有必要的话也必须要回到原位。
通过递增变量或者临时路径进行扩展,看情况,一般递增变量扩展时增加1,临时路径扩展时尾部增加一步。路径代表已做过的选择,可以是索引值index,也可以是链表。
做选择、扩展递归下一步、撤销选择。
伪代码:
void findSolutions(t, other params) :
//1,结束条件,一轮搜索结束,得到一个可行解
if (t > n) :
displaySolution();
return;
for (val = first to last) : //2,做选择
if (isValid(val, n)) : //3,剪枝
applyValue(val, n); //4,应用选择
findSolutions(n+1, other params); //5,向前纵向扩展
removeValue(val, n); //6,撤销选择
416.分割等和子集
转化为[0/1背包问题]:
class Solution {
public boolean canPartition(int[] nums) {
if (nums.length <= 1) {
return false;
}
int sum = 0;
for (int element : nums) {
sum += element;
}
if (sum % 2 != 0) {
return false;
}
Arrays.sort(nums);
return backtrack(nums.length - 1, sum / 2, nums);
}
public boolean backtrack(int index, int remainSum, int[] nums) {
if (index <0 || nums[index] > remainSum) {
return false;
}
if (nums[index] == remainSum) {
return true;
}
for (int i = 1; i >= 0; i--) {//i++会超时
remainSum -= nums[index] * i;
if (backtrack(index - 1, remainSum, nums)) {
return true;
}
remainSum += nums[index] * i;
}
return false;
}
}
为了套用模板,写的不够简洁,有更简洁的写法。
254因子的组合
与前一题的不同之处在于,单个解向量的元素个数是不确定的。
class Solution {
private List<List<Integer>> results = new ArrayList<>();
public List<List<Integer>> getFactors(int n) {
if (n <= 3) {
return results;
}
LinkedList<Integer> path = new LinkedList<>();
path.add(1);
backtrack(n, path);
return results;
}
private void backtrack(int remain, LinkedList<Integer> path) {
if (remain == 1) {
List<Integer> result = new LinkedList<>();
for (int ele : path) {
if (ele != 1) {
result.add(ele);
}
}
Collections.sort(result);
if (!results.contains(result) && result.size() != 1) {
results.add(result);
}
return;
}
for (int i = 2; i <= remain; i++) {
if (remain % i != 0) {
continue;
}
path.add(i);
remain = remain / i;
backtrack(remain, path);
path.removeLast();
remain = remain * i;
}
}
}
public class NQueens {
private List<List<String>> res = new LinkedList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(board[i], '.');
}
backtrack(0, board);
return res;
}
/**
* backtrack为标准的回溯法模板
*
* @param index 索引,或者已做选择的路径
* @param board
*/
private void backtrack(int index, char[][] board) {
//结束条件为所有的皇后都已放置
if (index == board.length) {
res.add(chars2StrList(board));//把可行解加入到结果集
return;
}
int cols = board[0].length;
//选择列表为每一列
for (int i = 0; i < cols; i++) {
if (!isValid(board, index, i)) {//剪枝
continue;
}
//做选择,在当前位置放置皇后
board[index][i] = 'Q';
//向纵深方向扩展一步
backtrack(index + 1, board);
//撤销选择
board[index][i] = '.';
}
}
private List<String> chars2StrList(char[][] board) {
List<String> strList = new LinkedList<>();
for (char[] row : board) {
strList.add(String.valueOf(row));
}
return strList;
}
private boolean isValid(char[][] board, int row, int col) {
//检查同一列有没有放置皇后
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') {
return false;
}
}
//检查左上角的对角线有没有放置皇后
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') {
return false;
}
}
//检查右上角的对角线有没有放置皇后
for (int i = row - 1, j = col + 1; i >= 0 && j < board.length; i--, j++) {
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
}
class Solution {
public String[] expand(String S) {
if (!S.contains("{")) {
return new String[] {S};
}
String s = S.replaceAll("\\{", " ").replaceAll("}", " ").replaceAll(",", "");
String[] strs = s.split(" ");
List<String> list = new ArrayList<>();
for (int i = 0; i < strs.length; i++) {
if (strs[i].length() != 0) {//"" length is 0
list.add(strs[i]);
}
}
List<String> listresult = new ArrayList<>();
LinkedList<Character> path = new LinkedList<>();
backtraceDfs(list, 0, path, listresult);
Collections.sort(listresult);
return listresult.toArray(new String[listresult.size()]);
}
void backtraceDfs(List<String> list, int index, LinkedList<Character> path, List<String> listresult) {
if (index == list.size()) {
StringBuilder builder = new StringBuilder();
for (int k = 0; k < path.size(); k++) {
builder.append(path.get(k));
}
listresult.add(builder.toString());
return;
}
for (int j = 0; j < list.get(index).length(); j++) {
path.add(list.get(index).charAt(j));
backtraceDfs(list, index + 1, path, listresult);
path.removeLast();
}
}
}
class Solution {
private List<String> result = new ArrayList<>();
public List<String> generatePalindromes(String s) {
if (s.length() < 1) {
return new ArrayList<String>();
}
Map<Character, Integer> map = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
Character key = s.charAt(i);
map.put(key, map.getOrDefault(key, 0) + 1);
}
Character ch = null;
int size = 0;
int count = 0;
for (Character c : map.keySet()) {
if (map.get(c) % 2 == 1) {
map.put(c, map.get(c) - 1);
ch = c;
count++;
}
size += map.get(c) / 2;
}
if (count > 1) {
return new ArrayList<String>();
}
char[] chars = new char[size];
int i = 0;
for (char c : map.keySet()) {
for (int j = 0; j < map.get(c) / 2; j++) {
chars[i++] = c;
}
}
palindrome2(chars, new StringBuilder(), ch, new boolean[size]);
return new ArrayList<>(result);
}
private void palindrome2(char[] chars, StringBuilder sb, Character ch, boolean[] visited) {
if (sb.length() == chars.length) {
StringBuilder temp = new StringBuilder(sb);
StringBuilder reverse = new StringBuilder(sb).reverse();
result.add(ch == null ? temp.append(reverse).toString() : temp.append(ch).append(reverse).toString());
return;
}
for (int i = 0; i < chars.length; i++) {
if (visited[i]) {
continue;
}
if (i > 0 && !visited[i - 1] && chars[i] == chars[i - 1]) {
continue;
}
sb.append(chars[i]);
visited[i] = true;
palindrome2(chars, sb, ch, visited);
sb.deleteCharAt(sb.length() - 1);
visited[i] = false;
}
}
}
回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,定义解空间树,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。
贪心法和动态规划法都要求问题最优解具有最优子结构特性。贪心法求解还要求设计最优量度标准,但这并非易事,这时候可以用回溯。回溯法是比贪心法和动态规划法更一般的方法。
年份 | 事件 | 相关论文 |
---|---|---|
1960 | R. J. Walker第一次尝试全面阐述回溯编程的范围和方法 | R. J. Walker(1960), An enumerative technique for a class of combinatorial problems. |
1965 | S. Golamb 和 L. Baumert.的方案进行修改提出.Backtrack programming | Golomb, S. W., & Baumert, L. D. (1965). Backtrack programming. Journal of the ACM (JACM), 12(4), 516-524. |
1976 | Stallman, R. M., & Sussman, G. J. 提出相关性回溯法 | Stallman, R. M., & Sussman, G. J. (1976). Forward reasoning and dependency-directed backtracking in a system for computer-aided circuit analysis. |
1993 | 动态回溯法被提出 | Ginsberg, M. L. (1993). Dynamic backtracking. journal of artificial intelligence research, 1, 25-46. |
2019年智能网络调度的几篇论文:
还有早些年几篇老古董文章,和传输协议相关:
1、Jingyu Wang, Jianxin Liao, Tonghong Li, Jing Wang: On the collaborations of multiple selfish overlays using multi-path resources. Peer-to-Peer Networking and Applications 8(2): 203-215 (2015) 对等网多路径
2、Jingyu Wang, Jianxin Liao, Tonghong Li: OSIA: Out-of-order Scheduling for In-order Arriving in concurrent multi-path transfer. J. Network and Computer Applications 35(2): 633-643 (2012) 解决乱序问题
3、Jianxin Liao, Jinzhu Wang, Tonghong Li, Jing Wang, Jingyu Wang, Xiaomin Zhu: A distributed end-to-end overload control mechanism for networks of SIP servers. Computer Networks 56(12): 2847-2868 (2012) SIP流量控制
4、Jianxin Liao, Jingyu Wang, Tonghong Li, Xiaomin Zhu: Introducing multipath selection for concurrent multipath transfer in the future internet. Computer Networks 55(4): 1024-1035 (2011) 多路径选择
5、Jingyu Wang, Jianxin Liao, Xiaomin Zhu: Latent Handover: A flow-oriented progressive handover mechanism. Computer Communications 31(10): 2319-2340 (2008) 多路径切换
6、Jianxin Liao, Jingyu Wang, Xiaomin Zhu: A multi-path mechanism for reliable VoIP transmission over wireless networks. Computer Networks 52(13): 2450-2460 (2008) 多路径协议
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.