【左程云 数据结构与算法笔记】P11 递归与动态规划

183 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情 

下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。

迪杰斯特拉算法改进

我们知道,在之前算法实现中,我们主要通过遍历得到下一个长度最小的节点,可以通过小根堆提升效率 需要自己维护一个小堆栈的操作,进行修改栈里节点上元素的值 代码实现

public static class NodeRecord {  
   public Node node;  
   public int distance;  
 
   public NodeRecord(Node node, int distance) {  
       this.node = node;  
       this.distance = distance;  
   }  
}  
 
public static class NodeHeap {  
   private Node[] nodes;  
   private HashMap<Node, Integer> heapIndexMap;  
   private HashMap<Node, Integer> distanceMap;  
   private int size;  
 
   public NodeHeap(int size) {  
       nodes = new Node[size];  
       heapIndexMap = new HashMap<>();  
       distanceMap = new HashMap<>();  
       this.size = 0;  
   }  
 
   public boolean isEmpty() {  
       return size == 0;  
   }  
 
   public void addOrUpdateOrIgnore(Node node, int distance) {  
       if (inHeap(node)) {  
           distanceMap.put(node, Math.min(distanceMap.get(node), distance));  
           insertHeapify(node, heapIndexMap.get(node));  
       }  
       if (!isEntered(node)) {  
           nodes[size] = node;  
           heapIndexMap.put(node, size);  
           distanceMap.put(node, distance);  
           insertHeapify(node, size++);  
       }  
   }  
 
   public NodeRecord pop() {  
       NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));  
       swap(0, size - 1);  
       heapIndexMap.put(nodes[size - 1], -1);  
       distanceMap.remove(nodes[size - 1]);  
       nodes[size - 1] = null;  
       heapify(0, --size);  
       return nodeRecord;  
   }  
 
   private void insertHeapify(Node node, int index) {  
       while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {  
           swap(index, (index - 1) / 2);  
           index = (index - 1) / 2;  
       }  
   }  
 
   private void heapify(int index, int size) {  
       int left = index * 2 + 1;  
       while (left < size) {  
           int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])  
                   ? left + 1 : left;  
           smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;  
           if (smallest == index) {  
               break;  
           }  
           swap(smallest, index);  
           index = smallest;  
           left = index * 2 + 1;  
       }  
   }  
 
   private boolean isEntered(Node node) {  
       return heapIndexMap.containsKey(node);  
   }  
 
   private boolean inHeap(Node node) {  
       return isEntered(node) && heapIndexMap.get(node) != -1;  
   }  
 
   private void swap(int index1, int index2) {  
       heapIndexMap.put(nodes[index1], index2);  
       heapIndexMap.put(nodes[index2], index1);  
       Node tmp = nodes[index1];  
       nodes[index1] = nodes[index2];  
       nodes[index2] = tmp;  
   }  
}  
 
//改进后的dijkstra算法  
public static HashMap<Node, Integer> dijkstra2(Node head, int size) {  
   NodeHeap nodeHeap = new NodeHeap(size);  
   nodeHeap.addOrUpdateOrIgnore(head, 0);  
   HashMap<Node, Integer> result = new HashMap<>();  
   while (!nodeHeap.isEmpty()) {  
       NodeRecord record = nodeHeap.pop();  
       Node cur = record.node;  
       int distance = record.distance;  
       for (Edge edge : cur.edges) {  
           nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);  
       }  
       result.put(cur, distance);  
   }  
   return result;  
}

暴力递归就是尝试

  1. 把问题转化为规模缩小了的同类问题的子问题
  2. 有明确的不需要继续进行递归的条件
  3. 有当得到了子问题的结果之后的决策过程
  4. 不记录每一个子问题的解 是动态规划的基础,需要多去尝试

汉诺塔问题

将1上圆盘按原来大小排列移动到3 最优解的步骤 按左边步骤进行移动,即可得到三层的汉诺塔问题最优解 进一步到n层 代码实现

public static void hanoi(int n) {  
    if (n > 0) {  
        func(n, n, "left", "mid", "right");  
    }  
}  
  
public static void func(int rest, int down, String from, String help, String to) {  
    if (rest == 1) {  
        System.out.println("move " + down + " from " + from + " to " + to);  
    } else {  
        func(rest - 1, down - 1, from, to, help);  
        func(1, down, from, help, to);  
        func(rest - 1, down - 1, help, from, to);  
    }  
}  
  
public static void main(String[] args) {  
    int n = 3;  
    hanoi(n);  
}

运行结果

尝试时,定一个统一的标准,尝试时先从局部拆问题,只要在局部下决策正确,总体就正确;递归从局部再到总体

打印一个字符串的全部子序列,包括空字符串

思路分析 从左到右都有要和不要两条路,从左往后一个一个字符试着判断 通过位置,判断要与不要,将之前形成的列表放进res中,当来到终止位置时,打印之前选择 核心代码实现

public static void process(char[] chs, int i, List<Character> res) {  
    if(i == chs.length) {  
        printList(res);  
    }  
    List<Character> resKeep = copyList(res);  
    resKeep.add(chs[i]);  
    process(chs, i+1, resKeep);  
    List<Character> resNoInclude = copyList(res);  
    process(chs, i+1, resNoInclude);  
}

打印字符串的全部排列

如字符串为abc时

  • 先分a和bc ,再将bc分为b,c或c,b
  • 先分b和ac,再将ac分为a,c或c,a
  • 先分c和ab,再将ab分为a,b或b,a 第一种情况有N种可能,第二种情况为N-1种可能...相当于一个n的排列 如果走到了数组长度则将其加进集合中,i往后所有的位置,不断交换,i往后所有的位置都可以来到i的位置,并进行走分支,最后交换回来 核心代码
public static void process(char[] chs, int i, ArrayList<String> res) {  
    if (i == chs.length) {  
        res.add(String.valueOf(chs));  
    }  
    boolean[] visit = new boolean[26];  
    for (int j = i; j < chs.length; j++) {      
        swap(chs, i, j);  
         process(chs, i + 1, res);  
         swap(chs, i, j);  
    }  
}

打印字符串的全部排列 要求不重复

虽然可以在打印所有排列的基础上做去重或者用一个set存起来,但是效率有点低 需要遍历所有的情况 这里是限制下一个走的路 指标上没有差距,但在常数项上有优化 相当于一个n的组合,只需要在原来代码上设置一个字符标识指定字母是否已经走过 visit[]数组记录每个字母是否试过,如visit[0]标志a是否已经试过 核心代码

public static void process(char[] chs, int i, ArrayList<String> res) {  
    if (i == chs.length) {  
        res.add(String.valueOf(chs));  
    }  
    boolean[] visit = new boolean[26];  
    for (int j = i; j < chs.length; j++) {  
        if (!visit[chs[j] - 'a']) {  
            visit[chs[j] - 'a'] = true;  
            swap(chs, i, j);  
            process(chs, i + 1, res);  
            swap(chs, i, j);  
        }  
    }  
}

通过判断指定字母是否已经试过,得到去重的全排列

拿牌比分数问题

思路分析:先拿最左边或最右边的牌,分析后手拿牌的情况 先手则判断最大收益,即拿走某一个牌后对面拿的牌分数最低; 后手函数则为最低收益,即对方会把最差的结果留下来 其实是一种动态规划思想的表现

public static int win1(int[] arr) {  
    if (arr == null || arr.length == 0) {  
        return 0;  
    }  
    return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));  
}  
  
public static int f(int[] arr, int i, int j) {  
    if (i == j) {  
        return arr[i];  
    }  
    return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));  
}  
  
public static int s(int[] arr, int i, int j) {  
    if (i == j) {  
        return 0;  
    }  
    return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));  
}

给一个栈,逆序这个栈,不能申请额外的数据结构,只能使用递归函数

需要先实现函数将栈底元素取出,主要利用递归栈保留信息将信息逆序 去栈底元素函数实现思路: 先取出栈顶元素,如果栈为空则返回,否则重新执行该函数(同样的栈),用lat接收,循环反复到栈底时,返回,执行下一步,将之前取出的元素重新压回栈中·,通过不断返回给last,最后返回的last即为栈底元素 代码实现

public static int getAndRemoveLastElement(Stack<Integer> stack) {
		int result = stack.pop();
		if (stack.isEmpty()) {
			return result;
		} else {
			int last = getAndRemoveLastElement(stack);
			stack.push(result);
			return last;
		}
	}

接着需要准备一个逆序函数 思路分析:逆序的实现思路与上面相似,也是利用递归栈保留信息

public static void reverse(Stack<Integer> stack) {  
    if (stack.isEmpty()) {  
        return;  
    }  
    int i = getAndRemoveLastElement(stack);  
    reverse(stack);  
    stack.push(i);  
}

字符与数字的对应

规定1和A对应... 26对应Z 所以大于2的数字无法与后面数字组合 如111可以转化为AAA,AK,KA(K为11) 给定数字组成的字符串,返回转化结果 思路分析:从左往后试每一个位置 若压中0字符,后面字符转换无效,为1或2可能有后续可以组合的情况 2后面必须<7 为3~9 整个方法结束

核心代码实现

public static int process(char[] chs, int i) {  
    if (i == chs.length) {  
        return 1;  
    }  
    if (chs[i] == '0') {  
        return 0;  
    }  
    if (chs[i] == '1') {  
        int res = process(chs, i + 1);  
        if (i + 1 < chs.length) {  
            res += process(chs, i + 2);  
        }  
        return res;  
    }  
    if (chs[i] == '2') {  
        int res = process(chs, i + 1);  
        if (i + 1 < chs.length && (chs[i + 1] >= '0' && chs[i + 1] <= '6')) {  
            res += process(chs, i + 2);  
        }  
        return res;  
    }  
    return process(chs, i + 1);  
}

袋子中装最大价值货物(Knapsack)

将所有情况按照要与不要展开

i往后形成的价值与前面无关

代码实现

public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {  
    if (alreadyweight > bag) {  
        return 0;  
    }  
    if (i == weights.length) {  
        return 0;  
    }  
    return Math.max(  
  
            process1(weights, values, i + 1, alreadyweight, bag),  
  
            values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag));  
}

总结

形式越少,数量越少,越容易改动态规划

实改动态规划的成败取决于试法,相比之下,试法更容易实现,更容易理解,总结经验 动态规划是试法不同总结出来的东西

关于动态规划与递归的理解

动态规划通常用于最优解问题,动态规划法和递归分治法的相同点就是将待求解问题分解成若干子问题,然后从这些子问题得到原问题的解。与分治法不同的是,动态规划法的问题,其分解得到的子问题往往不是互相独立的。若用分治法求解最优问题,往往分解得到的子问题过多,有些子问题会被重复计算。而动态规划法将已解决的子问题答案存下来,需要子问题答案时可以直接获得,从而达到逐步动态逼近最优解,避免重复计算,提高效率。