《算法导论第二版》CLRS 以及常用算法总结

一、各部分介绍

二、学习要求

  • 各主流算法可以默写。然后证明可以写出来为止
  • 先学习对应方法,简单思考后。不要一直对不熟悉的算法自己去思考,先去了解,熟练,再应用
  • 对于递归的方法练习一下翻译,即用非递归形式写
  • 已经学过的算法,快速写,然后理解,然后背,节约时间。

1 其他算法:

1 快速排序
  •  一般把起始点当作中心值base 
  • <= base 的点都移动到左边
  • >= base 的点都移动到右边
  • 注意计算顺序,先所右边还是左边会影响 l 和 r的位置,我先算r,再l。l <= base, r > base.
  • 最后把base赋予l
  • 然后切数组 s~l-1, r+1~e 
package algorithm;

public class QuickSort {
    public void quickSort(int [] nums, int s, int e){
        if(s >= e) return;
        int l = s, r = e, base = nums[s];
        while (l < r){
            while (l < r && nums[r] > base) r--;
            while (l < r && nums[l]<=base) l++;
            if(l< r){
                int temp = nums[l];
                nums[l] = nums[r];
                nums[r] = temp;
            }
        }
        nums[s]=nums[l];
        nums[l] = base;
        quickSort(nums, s, l-1);
        quickSort(nums, l+1, e);
    }

    public static void main(String [] args){

    }
}

  

1.1 快速选择 quickSelect

find kth number in an array

public int quickSelect(int s, int e, int k){
        if(s == e) return nums[s];
        int base = nums[e], L = s, R = s;
        while(R <= e) {
            if(nums[R] <= base) {
                swap(L, R);
                L++;
                R++;
            } else R++;
        }
        L--;
        if(L == k) return nums[L];
        else if(k < L) return quickSelect(s, L-1, k);
        else return quickSelect(L+1, e, k);
}
1.2 three partition
int L = 0, R = 0, e = nums.length - 1;
        while(R <= e) {
            if(nums[R] < k) {
                swap(L, R);
                L++;
                R++;
            } else if(nums[R] > k) {
                swap(R, e);
                R++;
                e--;
            } else R++;
}

 

2 归并排序
package algorithm;

public class MergeSort {
    public void merge(int [] nums, int s, int m, int e){
        if(s >= e) return;
        int [] tempNums = new int[e-s+1];
        int l = s, r = m+1, c = 0;
        while (l <= m && r <= e){
            if(nums[l] <= nums[r]) tempNums[c++] = nums[l++];
            else tempNums[c++] = nums[r++];
        }

        while (l <= m) tempNums[c++] = nums[l++];
        while (r <= e) tempNums[c++] = nums[r++];
        for(int i = s; i <= e; i++){
            nums[i] = tempNums[i-s];
        }
    }

    public void mergeSort(int [] nums, int s, int e){
        if(s >= e) return;
        int m = (s+e)/2;
        mergeSort(nums, s, m);
        mergeSort(nums, m+1,e);
        merge(nums, s, m, e);
    }
} 
 
3 堆排序
  • 由后向前创建最大堆
  • left = parent*2+1, right = parent*2+2,则0点为最大值
  • 把堆顶和堆低值互换,调整堆,每次长度-1
package algorithm;

public class HeapSort {
    public void headAdjust(int [] nums, int index, int n){
        int parent = index;
        while (parent < n){
            int left = parent * 2 + 1;

            if(left < n && left + 1 < n && nums[left] < nums[left+1]) left = left + 1;
            if(left < n && nums[parent] >= nums[left]) break;
            if(left >= n) break;

            int temp =nums[parent];
            nums[parent] = nums[left];
            nums[left] = temp;
            parent = left;
        }
    }

    public void heapSort(int [] nums){
        //create heap
        int parent = nums.length/2-1;
        for(; parent >= 0; parent--){
            headAdjust(nums, parent, nums.length);
        }

        for(int n = nums.length-1; n >= 1; n--){
            int max = nums[0];
            nums[0] = nums[n];
            nums[n] = max;
            headAdjust(nums, 0, n);
        }
    }
}

 

4 优先队列
  • 根据堆排序实现

  • extract操作,相当于堆排序并且把堆顶元素输出
  • insert操作,把元素放到最后,然后不断比较parent,并且交换元素
5 链表基本操作
6 二叉搜索树
  • 中序遍历排序

  • 插入、删除、最小、最大、改变父节点、左右子节点

7 线段树

  • 构建、查询、更新
  • 左孩子2*parent, 右孩子 2*parent +1
  • 构建,从低向上,递归. 
  • 延迟操作:增加一个标记mark。当更新或者查询范围完全包括了树的左、右范围,则mark增加要增加的值
  • 当遇到查询、更新时候就pushDown。
 1) 最大值线段树,动态开点,lazy标记, 需要区域更新才需要lazy标记,防止更新所有点,变成暴力。
/**
* Maximum segment tree
**/
public class SegmentTree {
// left index, right index, maximum, lazy
// maximum set tree, lazy is the maximum value of this subtree
class Node {
int ls, rs, val, lazy;
}

int N = (int) 1e9, cnt = 0;
Node[] tr = new Node[1000010];

public void update(int u, int ls, int rs, int l, int r, int v) {
//if in the range of l, r, back tracking
if (ls >= l && rs <= r) {
tr[u].val = v;
tr[u].lazy = v;
return;
}
// push down the maximum value to children
pushDown(u);
int mid = (ls + rs) / 2;
if (l <= mid) update(tr[u].ls, ls, mid, l, r, v);
if (r > mid) update(tr[u].rs, mid + 1, rs, l, r, v);
// update the current maximum
pushUp(u);
}

// ls, lr the node u range
// l, r is query range
public int query(int u, int ls, int rs, int l, int r) {
if (ls >= l && rs <= r) return tr[u].val;
// create node or push down the lazy mark
pushDown(u);
int res = 0, mid = (ls + rs) / 2;
// from root ls <= l
if (l <= mid) res = query(tr[u].ls, ls, mid, l, r);
// from root rs >= r
if (r > mid) res = Math.max(res, query(tr[u].rs, mid + 1, rs, l, r));
return res;
}

public void pushUp(int u) {
int l = tr[u].ls, r = tr[u].rs;
tr[u].val = Math.max(tr[l].val, tr[r].val);
}

public void pushDown(int u) {
// create node dynamically
if (tr[u] == null) tr[u] = new Node();
if (tr[u].ls == 0) {
tr[u].ls = ++cnt;
tr[tr[u].ls] = new Node();
}
if (tr[u].rs == 0) {
tr[u].rs = ++cnt;
tr[tr[u].rs] = new Node();
}

// process the lazy mark
if (tr[u].lazy == 0) return;
// left and right all update the maximum value to
tr[tr[u].ls].lazy = tr[u].lazy;
tr[tr[u].rs].lazy = tr[u].lazy;
tr[tr[u].ls].val = tr[u].lazy;
tr[tr[u].rs].val = tr[u].lazy;
// remove the lazy mark
tr[u].lazy = 0;
}
}

 

 

求和线段树, 一次性全开点,lazy标记,需要区域更新才需要lazy标记,防止更新所有点,变成暴力。
package com.lagou.edu.test;

/**
 * Maximum segment tree
 **/
public class SegmentTreeSum {
    // left index, right index, maximum, lazy
    // maximum set tree, lazy is the maximum value of this subtree
    class Node {
        int ls, rs, val, lazy;
    }

    int N = (int) 2e4, cnt = 0;
    Node[] tr = new Node[500000];

    public void update(int u, int ls, int rs, int l, int r, int v) {
        //if in the range of l, r, back tracking
        if (ls >= l && rs <= r) {
            tr[u].val += v * (rs-ls+1);
            tr[u].lazy += v;
            return;
        }
        // push down the update value to children
        pushDown(u, ls, rs);
        int mid = (ls + rs) / 2;
        if (l <= mid) update(tr[u].ls, ls, mid, l, r, v);
        if (r > mid) update(tr[u].rs, mid + 1, rs, l, r, v);
        // update the current maximum
        pushUp(u);
    }

    // ls, lr the node u range
    // l, r is query range
    public int query(int u, int ls, int rs, int l, int r) {
        if (ls >= l && rs <= r) return tr[u].val;
        // create node or push down the lazy mark
        pushDown(u, ls, rs);
        int res = 0, mid = (ls + rs) / 2;
        // from root ls <= l
        if (l <= mid) res = query(tr[u].ls, ls, mid, l, r);
        // from root rs >= r
        if (r > mid) res += query(tr[u].rs, mid + 1, rs, l, r);
        return res;
    }

    public void pushUp(int u) {
        int l = tr[u].ls, r = tr[u].rs;
        tr[u].val = tr[l].val + tr[r].val;
    }

    public void pushDown(int u, int ls, int rs) {
        // create node dynamically
        if (tr[u] == null) tr[u] = new Node();
        if (tr[u].ls == 0) {
            tr[u].ls = ++cnt;
            tr[tr[u].ls] = new Node();
        }
        if (tr[u].rs == 0) {
            tr[u].rs = ++cnt;
            tr[tr[u].rs] = new Node();
        }

        // process the lazy mark
        if (tr[u].lazy == 0) return;
        // left and right all update the maximum value to
        int mid = (ls + rs) / 2;
        int llen = mid - ls+1;
        int rlen = rs - (mid+1) + 1;
        tr[tr[u].ls].lazy += tr[u].lazy;
        tr[tr[u].rs].lazy += tr[u].lazy;
        tr[tr[u].ls].val += tr[u].lazy * llen;
        tr[tr[u].rs].val += tr[u].lazy * rlen;
        // remove the lazy mark
        tr[u].lazy = 0;
    }
}

 

3) 点单操作,最大值线段树,不需要lazy与pushdown
    class Node {
        // can be other
        long [] f = new long[4];
    }

    Node[] tr = new Node[n << 2 + 1];
    public void maintain(int u) {
        // can be other.
        long [] a = tr[u*2].f;
        long [] b = tr[u*2+1].f;
        tr[u].f[0] = Math.max(a[0] + b[2], a[1] + b[0]);
        tr[u].f[1] = Math.max(a[0] + b[3], a[1] + b[1]);
        tr[u].f[2] = Math.max(a[2] + b[2], a[3] + b[0]);
        tr[u].f[3] = Math.max(a[2] + b[3], a[3] + b[1]);
    }

    public void update(int u, int l, int r, int v, int i) {
        //if in the range of l, r, back tracking
        if (l == r) {
            tr[u].f[3] = Math.max(v, 0);
            return;
        }
        // push down the maximum value to children
        int mid = (l + r) / 2;
        if (i <= mid) update(u*2, l, mid, v, i);
        if (i > mid) update(u*2+1, mid + 1, r, v, i);
        // update the current maximum
        maintain(u);
    }

    public void build(int u, int l, int r, int [] nums) {
        if(tr[u] == null) tr[u] = new Node();
        if(l == r) {
            tr[u].f[3] = Math.max(nums[r], 0);
            return;
        }
        int m = (l+r)/2;
        build(u*2, l, m, nums);
        build(u*2+1, m+1, r, nums);
        maintain(u);
    }
 4) 最小值、最大值线段树
int [] st;

public void pushUp(int id) {
    st[id] = Math.min(st[id*2], st[id*2+1]);
}

public int query(int id, int ls, int rs, int l, int r) {
    if(l <= ls && rs <= r) return st[id];
    int mid = (ls+rs)/2;
    int res = inf;
    if(l <= mid) res = query(id*2, ls, mid, l, r);
    if(r > mid) res = Math.min(res, query(id*2+1, mid+1, rs, l, r));
    return res;
}


public void update(int id, int ls, int rs,  int pos, int val) {
    if(ls == rs) {
        st[id] = val;
        return;
    }
    int mid = (ls+rs)/2;
    if(pos <= mid) update(id*2, ls, mid, pos, val);
    else update(id*2+1, mid+1, rs, pos, val);
    pushUp(id, st);
}

 

8 红黑树 
9 拓扑排序
  • DAG,所有顶点的线性序列。1)每个顶点只出现一次 2)。
  • 如果存在一条边A-B,则顶点A出现在顶点B的前面。
  • 应用:任务依赖解析。依赖少的排在前面,依赖多的排在后面,然后按顺序输出任务。
package algorithm;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

public class TopologicalSort {
    int [] inDegrees;

    public void initInDegrees(int [][] graph){
        int v = graph.length;
        inDegrees = new int[v];
        for(int i = 0; i < graph.length; i++){
            for(int j = 0; j < graph[0].length; j++){
                if(graph[i][j] != 0) inDegrees[j]++;
            }
        }
    }

    public List<Integer> topologicalSort(int [][] graph) {
        int [] inDegrees = new int[graph.length];
        ArrayDeque<Integer> q = new ArrayDeque<>();
        List<Integer> res = new ArrayList<>();
        for(int i = 0; i < inDegrees.length; i++) if(inDegrees[i] == 0) q.push(i);
        while (!q.isEmpty()){
            int s = q.pop();
            res.add(s);
            for(int t = 0; t < graph[s].length; t++){
                if(graph[s][t] != 0){
                    inDegrees[t]--;
                    if(inDegrees[t] == 0) q.push(t);
                }
            }
        }
        return res;
    }
}

 

10 TRIE
  • 前缀树
  • 跟据前缀查找、统计单词、数字等可以用此数据结构
package algorithm;

import algorithm.objs.TreeNode;

public class Trie {
    public class TrieNode{
        public boolean isEnd = false;
        public TrieNode [] next = new TrieNode[26];
    }
    public TrieNode root = new TrieNode();

    public void insert(String word){
        TrieNode cur = root;
        for(char ch : word.toCharArray()){
            if(cur.next[ch-'a'] == null){
                cur.next[ch-'a'] = new TrieNode();
            }
            cur = cur.next[ch-'a'];
        }
        cur.isEnd = true;
    }

    public boolean find(String word){
        TrieNode cur = root;
        for(char ch : word.toCharArray()){
            if(cur.next[ch-'a'] == null) return false;
            cur = cur.next[ch-'a'];
        }
        return cur.isEnd;
    }

    public static void main(String [] args){
        Trie trie = new Trie();
        trie.insert("abc");
        System.out.println(trie.find("abcd"));
    }
}

 

 11 Treap
  • 一个BST和一个Heap。
  • 通过左旋、右旋来保持树是平衡的
  • 增加一个随机数RAND,来检查是否需要旋转。可以让 rand[root] > rand[left] && rand[root] > rand[right]  或者反过来
  • 增加计数器来判断子孩子的数目。
package algorithm;

public class Treap {
    public static int r= 2333;


    public int [] lc;//左孩子
    public int [] rc;//右孩子
    public int [] val;//
    public int [] ord;//优先值
    public int [] siz;//以x为根节点,子节点的数目
    public int [] w;//与节点相同的数目
    public int sz = 0, rt = 0;

    public int Rand(){
        r = r*232323%Integer.MAX_VALUE;
        return r;
    }

    public void pushUp(int x){
        siz[x] = siz[lc[x]] + siz[rc[x]] + w[x];
    }

    public void lRotate(int root){
        int temp = rc[root];
        rc[root] = lc[temp];
        lc[temp] = root;
        siz[temp] = siz[root];
        pushUp(root);
    }

    public void rRotate(int root){
        int temp = lc[root];
        lc[root] = rc[temp];
        rc[temp] = root;
        siz[temp] = siz[root];
        pushUp(root);
    }

    public void insert(int root, int x){
        if(root == -1){
            sz++;
            root = sz;
            siz[root] = w[root] = 1;
            val[root] = x;
            ord[root] = Rand();
            return;
        }

        siz[root]++;
        if(val[root] == x) w[root]++;
        else if(val[root] < x){
            insert(rc[root], x);
            if(ord[rc[root]] < ord[root]) lRotate(root);
        }else{
            insert(lc[root], x);
            if(ord[lc[root]] < ord[root]) rRotate(root);
        }
    }


    boolean del(int root, int x){
        if(root == -1) return false;
        if(val[root] == x){
            if(w[root] > 1){
                w[root]--;
                siz[root]--;
                return true;
            }
            if(lc[root] == -1 || rc[root] == -1){
                root = lc[root] + rc[root];
                return true;
            }else if(ord[lc[root]] < ord[rc[root]]){
                rRotate(root);
                return del(root, x);
            }else {
                lRotate(root);
                return del(root, x);
            }
        }else if(val[root] < x){
            boolean flag= del(rc[root], x);
            if(flag) siz[root]--;
            return flag;
        }else {
            boolean flag = del(lc[root], x);
            if(flag) siz[root]--;
            return flag;
        }
    }

    public int queryRank(int root, int x){
        if(root == -1) return 0;
        if(val[root] == x) return siz[lc[root]] + 1;
        else if(x > val[root]) return siz[lc[root]] + w[root] + queryRank(rc[root], x);
        else return queryRank(lc[root], x);
    }

    int queryNum(int root, int x){
        if(root == -1) return 0;
        if(x <= siz[lc[root]]) return queryNum(lc[root], x);
        else if(x > siz[lc[root]] + w[root]) return queryNum(rc[root], x - siz[lc[root]]- w[root]);
        else return val[root];
    }


}

 

12 凸包计算GrahamScan模板

步骤

  1. 找最下左点,当作极点。
  2. 跟据到极点的极角,由小到大排,如果极角相同按照距离由小到大排列。crossProduct(a, b) = a.x*b.y - b.x*a.y.  (如果a在b的逆时针方向,crossProduct < 0)
  3. 排序后的点,如果最后一个极角对应多个点,则交换上面所有点的顺序,按照到距离由大到小排列(上一步是由小到大)。
  4. 按照对点的排序顺序放入堆 前2 个点。从第三个点开始进行下一个步骤
  5. 遍历排序的点
  6. 标记堆的top与top-1为  a ,b,当前遍历的点为c
  7. 判断crossProduce(a->b , c->a)看c->a是否是逆时针方向。是则加入堆,遍历下一个点;else则 pop b。到步骤5.
package algorithm;

import java.util.*;

import static algorithm.GrahamScan.dis;

public class GrahamScan {
    Node apex = new Node(100, 100);
    class Node implements Comparable<Node>{
        int x, y;
        public Node(int x, int y){
            this.x = x;
            this.y = y;
        }

        public int compareTo(Node b){
            int vp = vectorProduct(apex, this, b);
            if(vp == 0) return Double.compare(dis(apex, this), dis(apex, b));
            else if(vp < 0) return -1; // a is left to b
            else return 1;
        }

        @Override
        public String toString(){
            return "x " + x + " y " + y;
        }
    }

    public static double dis(Node a, Node b){
        return Math.sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y));
    }

    //x1y2- x2y1
    public static int vectorProduct(Node a, Node b, Node c){
        return (b.x-a.x)*(c.y-a.y) - (c.x-a.x)*(b.y-a.y);
    }


    public ArrayDeque<Node> grahamScan(List<Node> lists){
        //最后一条边如果计较相等,则交换最后一边的顺序,距离大的排在前面,距离小的排在后面. 要不然
        Collections.sort(lists);
        int j = lists.size()-2;
        while (j >= 0){
            if(vectorProduct(apex, lists.get(j), lists.get(j+1)) == 0) j--;
            break;
        }
        j = j+1;
        for(int i = lists.size()-1; i != j; i++, j--){
            Node temp = new Node(lists.get(i).x, lists.get(i).y);
            lists.get(i).x = lists.get(j).x;
            lists.get(i).y = lists.get(j).y;
            lists.get(j).x = temp.x;
            lists.get(j).y = temp.y;
        }

        ArrayDeque<Node> q = new ArrayDeque<>();
        q.push(lists.get(0));
        q.push(lists.get(1));
        Node a = lists.get(0);
        for(int i = 2; i < lists.size(); ){
            Node b = q.peek();
            Node c = lists.get(i);
            int vp = vectorProduct(a, b, c);
            // System.out.println(vp + " " + a.x + " " + a.y + " " + b.x + " " + b.y + " " + c.x + " " + c.y);
            if(vp <= 0){
                q.push(c);
                a = b;
                i++;
            }else{
                q.pop();
                b = q.pop();
                a = q.pop();
                q.push(a);
                q.push(b);
            }
        }
        return q;
    }

    public int[][] outerTrees(int[][] trees) {
        List<Node> lists = new ArrayList<>();

        for(int [] cor : trees){
            if(cor[0] < apex.x || (cor[0] == apex.x && cor[1] < apex.y)){
                apex.x = cor[0];
                apex.y = cor[1];
            }
            lists.add(new Node(cor[0], cor[1]));
        }

        ArrayDeque<Node> q = grahamScan(lists);
        //last one
        System.out.println(vectorProduct(new Node(0,2), new Node(0,0), new Node(1,1)));
        int [][] res = new int[q.size()][2];
        int ind = 0;
        while(!q.isEmpty()){
            Node n = q.pop();
            res[ind++] = new int[]{n.x, n.y};
        }
        return res;
    }
}

 

 13 差分算法
  • 如果对一个数组的区间变化相同值
  • 如果最后得到这个数组
  • 可以使用查分算法
  • d[i] = a[i]-a[i-1]
  • a[i] = d[1] +... + d[i] = a[1] + a[2]-a[1] + a[3] - a[2] +... + a[i]-[i-1]
  • 如果对a[i]~a[j] +c -> d[i] += c. d[j+1] -= c. -> a[i] ~ a[j] 都含有d[i] 则这个区间都+c. a[j+1] 到最后都有 d[j+1] 则+c 与 -c相抵消,值不变。
14 欧拉路与欧拉环

欧拉路(在连通的基础上):

  • 有向图 有一个点的出度-入度=1,一个点的入度-出度=1,其余点入度=出度
  • 无向图 两个点的度为奇数,其余都是偶数

欧拉环(在连通的基础上)

  • 有向图 所有点入度=出度
  • 无向图 所有点的度相同

算法:Hierholzer

  • 任意一点v开始
  • 遍历其一条出边,并将其删除。
  • 遍历到的点在所有边都遍历完后,再将其加入(这样可以保证还有其他边没走到的时候,这个点是最后加入的点,也就是后通过的i点,然后再走其他)
15 矩阵乘法快速幂算法

一个矩阵的n次幂为 A*A....*A.一共乘了n次。

可以转化为 A^(2^x1)*A^(2^x2)...A(2^xi) . 其中 2^x1 + 2^x2+ ... +  2^xi = n。

  • 将n写成2进制形式 1001111000 exp。
  • 然后遇到1位时候则将结果于当前位的矩阵相乘。
  • 每次n向左移动一位,然后base算个平方为base加一位。

矩阵快速次幂,可以用来优化动态规划。

f(k)_0 = a*f(k-1)_0 + b*f(k-1)_1;

f(k)_1 = c*f(k-1)_0 + d*f(k-1)_1;

写成矩阵形式。

[{f(k)_0}, {f(k)_1}] = [{a,b}, {c,d}] * [{f(k-1)_0}, {f(k-1)_1}]

[{f(k)_0}, {f(k)_1}] = [{a,b}, {c,d}]**k * [{f(0)_0}, {f(0)_1}]

求 [{a,b}, {c,d}]**k 可以用矩阵快速幂加速

public long [][] matrixPow(long [][] base, long k){
        long [][] res = new long[][] {{1,0}, {0,1}};
        while(k > 0){
            if((k&1) == 1) {
                res = matrixMul(res, base);
            }
            base = matrixMul(base, base);
            k = k >> 1L;
        }
        return res;
    }

    public long [][] matrixMul(long [][] a, long [][] b){
        int m = a.length, n = b[0].length;
        long [][] res = new long[m][n];
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                for(int k = 0; k < a[0].length; k++){
                    res[i][j] = (res[i][j] + a[i][k]*b[k][j]%M)%M;
                }
            }
        }
        return res;
    }

 

16 基数排序
  • 由最低位到最高位排序。
  • 先把个位放入桶中。-> (没有10位的)然后挪入一边。10位放入桶->(没有百位)挪一边。以此类推最后得到一个排序的数组
  • 数值的位数远远小于数目的时候,基数排序要快。否则其他。其实就是比较 k 和 logn. 其中k是位数,n数目。时间复杂度 O(kn).
package algorithm;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

public class RapixSort {
    public List<Integer> radix(int [] nums, int digits){
        List<ArrayDeque<Integer>> digitsQ  = new ArrayList<>();
        ArrayDeque<Integer> arrayDeque = new ArrayDeque<>();
        List<Integer> res = new ArrayList<>();

        for(int i = 0; i <= 9; i++) digitsQ.add(new ArrayDeque<>());
        //0 1 2 3 4 5 6 7 8 9
        //push all.
        for(int num : nums){
            arrayDeque.offer(num);
        }

        for(int i = 1; i <= digits; i++){
            //arrange
            int max = (int)Math.pow(10, i-1);
            while (!arrayDeque.isEmpty()){
                int num = arrayDeque.poll();
                int remain = num/max;
                int pos = remain - remain/10*10;
                digitsQ.get(pos).offer(num);
            }

            //collect
            for(int d = 0; d < 10; d++){
                while (!digitsQ.get(d).isEmpty()){
                    int num = digitsQ.get(d).poll();
                    if(num >= max*10) arrayDeque.offer(num);
                    else res.add(num);
                }
            }
        }
        return res;
    }

    public static void main(String[]args){
        RapixSort rapidSort = new RapixSort();
        List<Integer> res = rapidSort.radix(new int[]{12,11}, 5);
        System.out.println(res);
    }
}

 

17 树状数组 
  1. 频繁单点插入
  2. 频繁区间求和
  3. 用binaryindexedtree.
  4. update. x += (x&-x)
  5. preSum x -= (x&-x)
  6. 1 index start
  7. 最低位 lowbit = x & (-x)
  8. 父节点 x + lowbit(x)
  9. 前一个根结点 x- lowbit(x)
package algorithm;

public class BinaryIndexedTree {
    private int [] bt;

    public void buildBT(int [] list){
        bt = new int[list.length+1];
        int n = bt.length;
        for(int i = 0; i < list.length; i++) bt[i+1] = list[i];
        for(int i = 1; i < n; i++){
            int j = i + (i&-i);// add last one
            if(j < n) bt[j] += bt[i];
        }
    }
//单点更新
    public void update(int idx, int delta){
        idx += 1;
        while (idx < bt.length){
            bt[idx] += delta;
            idx += (idx & -idx);
        }
    }
//前缀和
    public int preSum(int idx){
        idx += 1;
        int result = 0;
        while (idx > 0){
            result += bt[idx];
            idx -= (idx & -idx);
        }
        return result;
    }
}

2 DP:

  1. 自下向上和自上向下两种方法
  2. 将问题拆成子问题,子问题不再有其他问题
  3. 考虑状态变换,比如相等会怎么样啊、大于小于会怎么样啊、+1-1啊这种,有限状态变换、需要子问题怎么去变换
 1 LSC

 

package algorithm;

public class LCS {
    public int longestCommonSubsequence(char [] x, char [] y){
        int m = x.length, n = y.length;
        int [][] dp = new int[m+1][n+1];
        for(int i= 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                if(x[i-1] == y[j-1]) dp[i][j] = dp[i-1][j-1] +1;
                else{
                    dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
                }
            }
        }
        return dp[m-1][n];
    }

    public static void main(String [] args){
        char [] x = "ABCCAEF".toCharArray();
        char [] y = "AEFBCCC".toCharArray();
        LCS lcs = new LCS();
        StringBuilder sb = new StringBuilder();
        sb.reverse();
        System.out.println();
        System.out.println(lcs.longestCommonSubsequence(x,y));
    }
}

3 GREEDY

  1. 将最优化问题转化为:对其做选择后,可只剩下一个子问题需要求解
  2. 做出贪心选择后,原问题总存在最优解,即贪心选择总是安全的
  3. 做出贪心选择后,剩余的子问题满足:其最优解与贪心选择组合可得到原问题最优解,这样就得到最优子结构
  • 活动策划问题。动态规划到贪心的流程
  • 0-1 背包问题
  • 分数背包问题
  • 霍夫曼编码 (了解形成过程)
  • 拟阵与势能了解

 1 背包问题

  • 动态规划.  dp[i][v] = max(dp[i-1][v],  dp[i-1][v-volume[i]] + value[i]).
  • dp[i][v]代表,背包体积v中,前1~i个物品最大价值
  • i from 1 to n.  v from 1 to maxV
  • 最后用一维数组代替二维数组 对空间进行优化。

1)0-1背包。选或者不选。对存在的内容由大到小滚动更新。

        for(int num : rewardValues){
            for(int i = num-1; i >= 0; i--){
                dp[i+num] = dp[i+num] || dp[i];
            }
        }    

2) 01背包,用状态压缩计算能构成和的数目个数,仅限nums数组个数少的情况

       for(int num : nums){
            for(int j = sum; j >= num; j--){
               dp[j] |= dp[j-num] << 1;
            }
            dp[num] |= 1;
        }    

 

3)完全背包

 

4)multiple knapsa

Optimize with range sum O(n * m)
• only have k items, m is value
• dp[i][v + k*m] = dp[i-1][v + k*m] + dp[i-1][v+(k-1)m] + ... dp[i-1][v]; A
• dp[i][v + (k+1)*m] = dp[i-1][v+(k+1)*m] + ... dp[i-1][v+m]; B
• B - A == > dp[i][v+(k+1)*m] = dp[i-1][v+(k+1)*m] + dp[i][v+k*m] - dp[i-1][v]
dp[i-1][v+km] is not use any .. etc

 

5)利用BitSet优化0-1背包

把比当前小的子集整体又移。

BigInteger f = BigInteger.ONE;
        int m2 = nums.size() > 1 ? nums.get(nums.size()-2) : 0;
        if(m2 + max == max*2-1) return max*2-1;
        for(int num : nums){
            BigInteger mask = BigInteger.ONE.shiftLeft(num).subtract(BigInteger.ONE);
            f = f.or(f.and(mask).shiftLeft(num));
        }
return f.bitLength()-1;

 

4 B树

  1. 了解磁盘工作过程
  2. 了解B数结构,插入,分裂,删除流程。不写了。

5 斐波那契堆

6 Van Emde Boas树

7 图计算

1)最小生成树:

1 kruskal (无向图。贪心加边)
  • 把所有边进行排序,由小到大
  • 每次取一条边加入到集合中,如果加入的边不构成环,则加入集合。
  • 并查集算法判断新加入的边是否会构成环 (写错了,父节点那里)
package algorithm;

import algorithm.objs.Edge;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Kruskal {
    public static int M = 0x7ffff;

    public List<Edge> kruskal(List<Edge> edges, int vertexNum){
        Collections.sort(edges);
        UnionFind unionFind = new UnionFind(vertexNum);
        List<Edge> res = new ArrayList<>();
        int count = 0;
        for(Edge edge : edges){
            if(unionFind.find(edge.s) == unionFind.find(edge.e)) continue;
            unionFind.merge(edge.s, edge.e);
            res.add(edge);
            if(count == vertexNum-1) break;
        }
        return res;
    }
}

 

2 prim(无向图,贪心加顶点)
  • 每次加入一个顶点 v到S中,S是V的子集。
  • 加入点的条件是,S中选择一个点,然后在V-S中找一点 k,使得边 edge(v, k)值最小
  • 贪心思想
package algorithm;

import algorithm.objs.Edge;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Prim {
    public static int M = 0x7ffff;
    public List<Edge> prime(int [][] weight){
        List<Edge> res = new ArrayList<>();
        int vertexNum = weight.length;
        boolean [] visited = new boolean[vertexNum];
        visited[0] = true;

        for(int k = 1; k < vertexNum; k++){
            int min = M, from = 0, to = 0;
            for(int i = 0; i < vertexNum; i++){
                if(!visited[i]) continue;
                for (int j = 0; j < vertexNum; j++){
                    if(visited[j]) continue;
                    if(min > weight[i][j]){
                        from=i;
                        to = j;
                        min = weight[i][j];
                    }
                }
            }
            visited[to] = true;
            res.add(new Edge(from, to, min));
        }

        return res;
    }


}

 

3 并查集 (无向图)
  • 把一棵树的点设置到一个公共父节点
  • 如果两棵树能合并到一起,则把高度小的树合并到高度大的数(rank1 <= rank2)。以最大程度保证更新父节点时候更新少数点(高度计算不绝对保证)。

 

package algorithm;

public class UnionFind {
    public int [] f;
    public int [] rank;

    public UnionFind(){}

    public UnionFind(int vertexNum){
        f = new int[vertexNum];
        rank = new int[vertexNum];
        for(int i = 0; i < vertexNum; i++){
            f[i] = i;
            rank[i] = 1;
        }
    }

    public int find(int x){
        int end = f[x];
        while (f[end] != end) end = f[end];
        while (f[x] != end){
            int fax = f[x];
            f[x] = end;
            x = fax;
        }
        return end;
    }

    public void merge(int x, int y){
        int fax = find(x), fay = find(y);
        //not equal. merge
        if(rank[x] <= rank[y]){
            f[fax] = fay;
            rank[y] += rank[x];
        }else {
            f[fay] = fax;
            rank[x] += rank[y];
        }
    }
}

 

4 朱刘算法 (有向图)
  • 有向图最小生成树,从根节点出发能到达所有其他点
  • 初始化,保存所有点入边权重最小值,然后所有点入边的权用最小值替代(即最小权边为入边)
  • 最小权重加上所有点入边的权重
  • 把有向图的环当成一个点,环需要一个入边。w -= In(v). In(v) 入边最小,把环内最小的权值删了避免重复
  • 去环操作,把不成环的边权 -= 最小入边权
  • 不断收缩直到没环
package algorithm;

import algorithm.objs.Edge;

import java.util.ArrayList;
import java.util.List;

public class ZhuLiu {
    public int zhuliu(int [][] graph, int root){
        //初始化设置
        int vertexNum = graph.length;
        List<Edge> edges = new ArrayList<>();

        for(int i = 0; i < vertexNum; i++){
            for(int j = 0; j < vertexNum; j++){
                if(graph[i][j] != Prim.M){
                    edges.add(new Edge(i,j,graph[i][j]));
                }
            }
        }


        int minCost = 0;

        //重复删除环和重复入边
        while (true){
            //入权初始化
            int [] inCost = new int[vertexNum];
            int [] pre = new int[vertexNum];
            int [] circleNo = new int[vertexNum];
            int totalCircleNum = -1;
            for(int i = 0; i < vertexNum; i++) {inCost[i] = Prim.M; circleNo[i] =-1;}

            for(Edge edge:edges){
                if(edge.s != edge.e && edge.e != root && edge.w < inCost[edge.e]){
                    pre[edge.e] = edge.s;
                    inCost[edge.e] = edge.w;
                }
            }

            inCost[root] = 0;
            pre[root] = root;

            //是否有入边检查
            for(int i = 0; i < vertexNum; i++){
                if(i != root && inCost[i] == Prim.M) {
                    return -1; // 无入边返回
                }
            }

            //遍历所有点看是否有环


            for(int i =0; i <vertexNum; i++) {
                if(i == root) continue;
//                System.out.println(inCost[i] + " " + vertexNum);
                minCost += inCost[i];
                int s = pre[i];
                //找是否有环
                while (s != i && s != root && circleNo[s] == -1){
                    s = pre[s];
                }

                //有环, 给环打标

                if(s != root && circleNo[i] == -1){
                    circleNo[i] = ++totalCircleNum;
                    while (pre[s] != i){
                        s = pre[s];
                        circleNo[s] = totalCircleNum;
                    }
                }
            }


            if(totalCircleNum == -1) break; // 无环返回
            //给剩余的点加上环标签
            for(int uu = 0; uu < vertexNum; uu++){
                if(circleNo[uu] == -1) {
                    circleNo[uu] = ++totalCircleNum;
                }
            }

            //如果有环则更新权值, 并且把环的入边,出边统一
            for(Edge edge : edges){
                int e = edge.e;
                edge.s = circleNo[edge.s];
                edge.e = circleNo[edge.e];

                if(edge.s != edge.e) edge.w -= inCost[e];
            }

            //更新边数和点数
            vertexNum = totalCircleNum + 1;
            root = circleNo[root];
        }

        return minCost;
    }
}

 

2)单源最短路径 (源节点S,到V的距离,现有一条边u,v,松弛操作RELAX了解下)

1 松弛操作性质:
  1. 最短路径满足三角不等式原理
  2. 距离是最短路径的上界
  3. 非路径性质
  4. 路径松弛性质: (s = v0, t = vk, 如果一个最短路径为 (v0,v1,... , vi,... vk, 则 如果有按次顺序的松弛操作 (vo, v1),  (v1,v2),... , (vk-1, vk) 则 vk.d = delta(s, vk)。次最短路径的松弛结果与其他松弛操作无关。即只要按顺序执行过 松弛操作 (vo, v1),  (v1,v2),... , (vk-1, vk) . 则就能得到最短路径。
  5. 前驱子图性质
2 Dijkstra 有向图无负权重
  • 以原点为起点,每次松弛上一次最小的距离的节点 j . relax(g, j, k).
  • 当前节点 路径最短,可以用当前节点进行松弛操作,因为最短路径,一定是由子最短路径不断松弛得来的。
  • 贪心思想,每次会产生一个节点为k的最短路径, 由j的松弛操作产生。 反证法 (存在f在V-S中,有更短的路径 )
package algorithm;

// 设点数为v,s为源点,t为终点
// 循环v-1次,每次加入一个点,该点为 delta(s,k)
// 每次循环对上一次最小的 V-S 集合中的 k,进行松弛操作

public class Dijkstra {
    private static int M = 0x7ffffff;

    public int [] dijkstra(int [][] weight, int source){
        int vertexNum = weight.length;
        boolean [] visited = new boolean[vertexNum];
        int [] distance = new int[vertexNum];
        if(vertexNum == 0) return distance;
        for(int i = 0; i < vertexNum; i++) distance[i] = M;
        distance[source] = 0;

        //vertexNum-1 relax. find all vertexes. must find source
        for(int i = 0; i < vertexNum; i++){
            //find min.
            int min = M, k = 0;
            for(int j = 0; j< vertexNum; j++){
                if(!visited[j] && min > distance[j]){
                    min = distance[j];
                    k = j;
                }
            }
            if(min == M) break;
            relax(distance, weight, k);
            visited[k] = true;
        }
        return distance;
    }

    public void relax(int [] distance, int [][] weight, int k){
        for(int i = 0; i < distance.length; i++) distance[i] = Math.min(distance[i], distance[k] + weight[k][i]);
    }

}

 

3 Bellman-ford 有向图有负权重。可以检测负权回路
  • 根据松弛操作性质路径操作松弛性质循环节点次数,每次对所有边进行遍历,则一定能找出所有的最短路径松弛操作组合。
  • 最后一次对所有边进行遍历,如果还能进行松弛操作,则有负环 
package algorithm;


import algorithm.objs.Edge;
import algorithm.objs.Graph;

import java.util.InputMismatchException;
import java.util.List;

public class BellmanFord {
    public static int M = 0x7fffffff;


    public int [] bellmanFord(Graph graph, int source){
        int vertexNum = graph.vertexNum;
        int [] distance = new int[vertexNum];

        for(int i = 0; i < vertexNum; i++) distance[i] = M;
        distance[source] = 0;
        //n次对每条边进行松弛,则最短路径一定确定 跟据松弛操作的性质
        for(int i = 0; i < vertexNum; i++) relax(graph.edges, distance);
        //负环判断
        for(Edge edge : graph.edges) if(distance[edge.e] > distance[edge.s] + distance[edge.e]) return new int[0];
        return distance;

    }

    public void relax(Edge [] edgeList, int [] distance) {
        for (Edge edge : edgeList) distance[edge.e] = Math.min(distance[edge.e], distance[edge.s] +edge.w);
    }

}

 

4 SPFA
差分系统的约束解。要会证明,bellman-ford可以解。然后差分系统的矩阵表达形式了解下

3)所有节点对的最短路径问题

1 FLOYD 动态规划 (如果所有边的权重为1,则是传递闭包问题,即判断图中两点是否连通)
  • 根据所有点子集 (1,....k) 判断,根据点k是否在最短路径中进行分析。即每次加入一个k点,和之前的 1....k-1点集合最短距离比较
  • 写出递归表达式。
  • 可以数学归纳法证明下。其他方法证明我觉得比较难。
  • 虽然DP公式比较好理解. 如果i到j的最短路径只包含中间节点 1~k。 dp[i][j][k] = dp[i][k][k-1] + dp[k][j][k-1]. dp[i][k][k-1]最短路径只包含1~k-1中间节点. dp[k][j]的中间节点同理。动态规划公式没什么问题。
package algorithm;

public class Floyd {
    public int[][] floyd(int [][] weight){
        int vertexNum = weight.length;
        int [][] dis = new int[vertexNum][vertexNum];

        for(int i = 0; i < vertexNum; i++){
            for(int j = 0; j < vertexNum; j++){
                dis[i][j] = weight[i][j];
            }
        }

        for(int k = 0; k < vertexNum; k++){
            for(int i = 0; i < vertexNum; i++){
                for(int j = 0; j < vertexNum; j++){
                    if(i != j){
                        dis[i][j] = Math.min(dis[i][j], dis[i][k] + dis[k][j]);
                    }
                }
            }
        }

        return dis;
    }
}
2 用于稀疏图的Johnson算法 (ford检查负环,然后更新权重,然后用DJ算)

 

4)最大流 (流网络,源节点,汇点,如果有反平行的边,拆成一个中间节点过度下)

  • 单个s,t
  • 多个s,多个t,定义超级原点s,超级汇点t
  • Ford-Fullkerson, 残存网络,增广路径和切割。抵消操作
1 ford-fullkerson
  • 残存容量
  • 有残存网络,则相当于以前走过的流量可以从反向走。即谁都可以占用当前方向。
  • bfs 在残存网络找一条增广路径,把所有可能点都压入队列头,遍历寻找
  • residualGraph[u][v] -= d, residualGraph[v][u] += d
  • bfs(s,t)
package algorithm;

import java.util.ArrayDeque;

public class FordFulkerson {
    int [][] residualGraph;
    int [] pre;
    boolean [] used;
    int vertexNum;

    public int fordFulkerson(int s, int t){
        int res = 0;
        while (bfs(s,t )){
            int minFlow = 0x7fffff;
            int u = 0, v= 0;
            for(int i = 0; i < vertexNum; i++){
                minFlow = Math.min(minFlow,residualGraph[i][pre[i]]);
            }

            for(int i = 0; i < vertexNum; i++){
                residualGraph[i][pre[i]] -= minFlow;
                residualGraph[pre[i]][i] += minFlow;
            }
        }
        return res;
    }

    boolean bfs(int s, int t){
       used = new boolean[vertexNum];
       ArrayDeque<Integer> q = new ArrayDeque<>();
       q.push(s);
       used[s] = true;
       while (!q.isEmpty()){
           int v = q.pop();
           for(int i = 0; i < vertexNum; i++){
               if(!used[i] || residualGraph[v][i] >0){
                   used[i] = true;
                   pre[i] = v;
                   if(i == t) return true;
                   q.push(i);
               }
           }
       }
        return false;
    }
}

 

5)二分图最大匹配

1 匈牙利算法
  • 为二分图左侧节点去找右侧匹配
  • 遍历左侧节点
  • 然后遍历右侧节点,看右侧是否能让出来

 

package algorithm;

public class Hungary {
    int [][] graph;
    int [] vt;
    boolean [] visited;
    boolean find(int x){
        for(int i = 0; i < graph.length; i++){
            if(visited[i] && graph[x][i] != 1) continue;
            visited[i] = true;
            if(vt[i] == -1 || find(vt[i])){
                vt[i] = x;
                return true;
            }
        }
        return false;
    }

    public void hungary(){
        for(int i = 0; i < vt.length; i++){
            if(vt[i] == -1) find(i);
        }
    }
}

 

2 KM算法
  • 为x,y侧分别分配顶标,x侧为最大期望值 
  • 为y侧设置一个期望差值数组
  • 用匈牙利算法判断每次是否能找到 gap为0的匹配对,并且为每个y设置一个最小期望值
  • 如果x找到匹配返回,找不到则用最小gap,去寻找
  • 用最小gap更新x侧期望值,y侧期望值,期望差值数组

 

package algorithm.learn;


import static algorithm.learn.BellmanFord.M;

public class KM {
    int n;

    int [][] graph;
    int [] ey;
    int [] ex;
    boolean [] visitedY;
    boolean [] visitedX;

    int [] yMatch;
    int [] slack;

    boolean find(int x){
        visitedX[x] = true;

        for(int y = 0; y < n; y++){
            if(visitedY[y]) continue;

            int gap = ex[x] + ex[y] - graph[x][y];
            if(gap == 0){ //找到可以match的
                visitedY[y] = true;
                if(yMatch[y] == -1 || find(yMatch[y])){
                    yMatch[y] = x;
                    return true;
                }
            }else {
                slack[y] = Math.min(slack[y], gap); //更新最小退步 的y
            }
        }
        return false;
    }

    int KM(){
        int res = 0;
        for(int i = 0; i < n; i++) yMatch[i] = -1;

        for(int y = 0; y < n; y++){
            for(int x = 0; x < n; x++){
                ey[y] = Math.max(ey[y], graph[x][y]);
            }
        }

        for(int i = 0; i < n; i++){
            for(int x = 0; x < n; x++) slack[x] = M;

            while (true){
                for(int x = 0; x < n; x++){
                    visitedY[x] = false;
                    visitedX[x] = false;
                }

                if(find(i)) break;

                int minSlack = M;
                for(int y = 0; y < n; y++){
                    if(!visitedY[y]) minSlack = Math.min(minSlack, slack[y]); // 找最小的slack
                }

                for(int j = 0; j < n; j++){
                    if(visitedX[j]) ex[j] -= minSlack;
                    if(visitedY[j]) ey[j] += minSlack;
                    else slack[j] -= minSlack; //差距减小,因为左侧x的期望减小
                }
            }
        }

        for(int i = 0; i < n; i++) res += graph[yMatch[i]][i];
        return res;
    }
}

 

 5) LAC两种求法

Tarjan

  • dfs
  • 后序遍历病查集更新父节点
  • 重写查询集合
  • 每次dfs一次节点,需要遍历该节点的query

核心代码

public void dfs(int node, int fa){
        v[node] = true;
        for(int [] ch : tree[node]){
            if(ch[0] != fa){
                dfs(ch[0], node);
                f[ch[0]] = node;
            }
        }

        for(int [] q : qs[node]) {
        // q[1] 是 ind
       // q[0] 是另一个端点
        // find(q[0]) 就是 node和q[0]的 LCA
if(v[q[0]]) getRes(q[0], node, find(q[0]), q[1]); } }

 

倍增

  • ST表 st[i][j] 代表 i + 2**j, st[i][j] = st[st[i]][j-1][j-1]
  • 利用ST表的性质,向上更新父节点。from i -> m :: fa[node][i] = fa[fa[node][i-1]][i-1]
  • 分别计算 节点 a, b的深度,深度不同需要 把较深的向上移动。然后一起向上移动到公共节点
public  void query(){
        //离线查询
        for(int i = 0; i < queries.length; i++){
            int a = queries[i][0], b = queries[i][1];
            if(dep[a] < dep[b]) {
                int temp = a;
                a = b;
                b = temp;
            }
            //移动至深度相同
            for(int j = 30; j >= 0; j--){
                if((1<<j) <= dep[a] - dep[b]) {
                    a = pa[a][j];
                }
            }
            //贪心找LCA
            for(int j = 30; j >= 0 ;j--){
                if(pa[a][j] != pa[b][j]){
                    a = pa[a][j];
                    b = pa[b][j];
                }
            }
            //如果a、b相同 不用再向上。 pa[a][x] 相同,经过贪心为 pa[x][x] + 2**x-1, 再向上移动一位位LCA 
            int lca = a;
            if(a != b) {
                lca = pa[a][0];
            }
            getRes(queries[i][0], queries[i][1], lca, i);
        }
    }
// 更新父节点
public void dfs(int node, int fa){
        for(int [] ch : tree[node]){
            int chn = ch[0];
            if(chn != fa){
                pa[chn][0] = node;
                // st表 动态规划更新父节点,父节点以及下层节点都已经算过
                for(int i = 1; i < 31; i++){
                    int pp = pa[chn][i-1];
                    pa[chn][i] = pa[pp][i-1];
                }
                dfs(chn, node);
            }
        }
    }

 

平面求最近点对

  • 所有点按照x排序
  • 以x轴话中心线,求左右两部分的最短距离。
  • 分治
  • 所有子问题中, 最短距离为d
  • 那么当前中心线,左右两部分点的横坐标到中心线 不会超过 d。
  • 收集所有点 cand
  • 对 cand 按照 y 坐标进行排序
  • 从 0 to n 对cand 进行两次 for 循环 找当前跨中心线最短距离,并不断更新d
  • 复杂度 O(nlogn), 可以证明每次遍历的点个数不超过 5 ? 6,
public int divideConqer(int s, int e) {
        if(s == e) return MAX;
        if(s+1== e) {
            int d = getManHattanDis(points.get(s), points.get(e));
            return d;
        }
        // get min from left and the right parts according to the middle line
        int m = (s+e)/2;
        int d = Math.min(divideConqer(s, m), divideConqer(m+1, e));
        // current find the min distance that less than d.
        List<int []> cand = new ArrayList<>();
        for(int i = s; i <= e; i++) {
            int [] p1 = points.get(i);
            int [] p2 = points.get(m);
            if(Math.abs(p1[0] - p2[0]) <= d) cand.add(p1);
        }
        Collections.sort(cand, (a,b)->{
            return a[1] - b[1];
        });
        for(int i = 0; i < cand.size(); i++) {
            for(int j = i+1; j < cand.size() && cand.get(j)[1] - cand.get(i)[1] <= d; j++) {
                int [] p1 = cand.get(i);
                int [] p2 = cand.get(j);
                int curd = getManHattanDis(p1, p2);
                d = Math.min(d, curd);
                int [] pair = getPari(p1[2], p2[2]);
            }
        }
        return d;
    }

 

8 多线程算法 (spawn, sync, 竞争条件)

  • 斐波那契数列举例
  • 多线程矩阵乘法(分治,将矩阵划分为子矩阵)

9 数论:

  • 约数
  • 素数
  • 合数
  • 最大公约数,互质数(最大公约数为1),a是d的倍数 d|a
  • 唯一因子分解定理。a = pi^ei * ... pr^er.  pi 为素数
  • gcd(a,b) = gcd(b, a mod b).  a = cx,  b = cy,  a = nb + m,  m = cx - cyn. 因此  m 、 b 有相同公约数 c.  m, b 的最大公约数假设是C,a=nb + m 可知, a 也有公约数 C,则 C = c。 
public int gcd(int a, int b) {
        if(a < b) return gcd(b,a);
        while(b != 0) {
            int temp = a;
            a = b;
            b = temp%b;
        }
        return a;
}
  • lcm = a*b / gcd(a,b). 
  • 从大到小按位枚举子集:sub = (sub-1) & s, 这种情况非空,如果是空,需要加一个判断 sub == s?
KMP
  • 需要寻找的字符为s,模版是p
  • 用p去计算 next数组
  • next数组存放,在p中,第j个元素最长的相同前、后缀长度。
  • 有了next数组,就可以用这个长度,递归去计算现在要匹配的. s[i], p[j]
package algorithm;

import java.util.Locale;

public class KMP {
    private int [] next;

    public void getNext(char [] p){
        int len = p.length;
        next = new int[len+1];
        next[0] = -1;
        //将p当作要匹配的字符串,然后用p的起点去对齐,计算前缀与后缀的match程度,

        int i = 0, j = -1;
        while (i != len){
            if(j == -1 || p[i] == p[j]){
                i++;
                j++;
                next[i] = j;
            }else{
                j = next[j];
            }
        }
    }

    public int kmp(char [] s, char [] p){
        getNext(p);
        int i = 0, j = 0;
        while (i != s.length && j != p.length){
            if(j == -1 || s[i] == p[j]){
                i++;
                j++;
            }else{
                j = next[j];
            }
        }

        if(j == p.length) return i-j;
        else return -1;
    }
}

 

扩展KMP

求一个字符串s任意一位开始,与字符串p的最长公共前缀长度,也叫 z algorithm. 算法步骤看注解,就是尝试从已经遍历过的 l, r中找有没有重复答案,并直接赋值,不断更新 l, r 的过程。

//总的时间复杂度O(n),外循环n,内循环不断扩展r,所以也为n。
    public void zFunc(String s) {
        char [] arr = s.toCharArray();
        int n = s.length();
        // z[i] s中下标为i,最长前缀长度
        int [] z = new int[n];
        int l = 0, r = 0;
        for(int i = 1; i < n; i++){
            // l     i   r
            // z[i'] 全部计算过,其中 i' < i
            // 如果 z[i'] < r-i+1, 直接赋值
            if(i <=r && z[i-l] < r-i+1){
                z[i] = z[i-l];
            } else {
            // 如果 z[i'] >= r-i+1
            // 则 z[i]至少 r-i+1, 然后增加 r     
                z[i] = Math.max(0,r-i+1);
                while(i+z[i] < n && arr[i+z[i]] == arr[z[i]]) z[i]++;
            }
            // 超出上限,则更新l,r
            if(i+z[i]-1 > r) {
                l = i;
                r = i+z[i]-1;
            }
        }
    }

 

循环字符串

1)  一个字符串是由子串重复构成,那么满足 s+s 去掉 第一个字符,最后一个字符,仍然包含s。证明:

假设 重复了n次。n*2 - 2 >= n.  n>=2, 所以至少重复2次才有这个性质。

2)最小的循环部分可以 kmp或者z func求。假设第一次从 s 的子串 匹配前缀直到 s的结束,那么剩下的前缀就是最小。
3)移动某些字符,看能构成另一个字符多少次,可以 s+s,在里面kmp或者 z 函数找。

4)z((i+n)%n) = z(i). 可以利用这个性质 求循环的相关问题

 

质数:

求1~n中的质数

1)暴力。 1~n**0.5, 看是否整出. O(n*√n

2) 埃式筛

update every prime. from (prime to n). p[prime*j] = k

时间复杂度 O(nlog(n))

class Solution {
    public int countPrimes(int n) {
        int res = 0;
        boolean [] isNotPrime = new boolean[n];
        int sqr = (int)Math.pow(n, 0.5);
        for(int i = 2; i < n; i++) {
            if(!isNotPrime[i]) {
                res++;
                for(int j = i; i <= sqr && j*i < n; j++) {
                    isNotPrime[j*i] = true;
                }
            }
        }
        return res;
    }
}

 

3) 线性筛法

任何一个数都是由质因数组成。假设当前数是 n = axbycz...   (现有质数 a, b, c ...由小到大排序,为已有的质因数),按照顺序,如果下一个数是合数,则一定是 ax+1bycz...   。 另一种情况是假设当前数是 n = cz... (现在有质数 a,b,c... 由小到大排序), 则下三个出现的合数是 acz... 和 bcz...  和 cz+1... 。  所以如果每一次,质数由小到大去更新和数,则一定可以把后面访问到的合数更新到。到 n % prime == 0, 就可以停止更新。 时间复杂度 O(n)

class Solution {
    public int countPrimes(int n) {
        int res = 0;
        int size = 0;
        int [] primes = new int[n];
        boolean [] isNotPrime = new boolean[n];

        for(int i = 2; i < n; i++) {
            if(!isNotPrime[i]) {
                res++;
                primes[size++] = i;
            }
            for(int j = 0; j < size && primes[j] * i < n; j++) {
                int next =  i * primes[j];
                isNotPrime[next] = true;
                if(i %  primes[j] == 0) break;
            }
        }
        return res;
    }
}

 

空间复杂度 O(1) 赋值,奇、偶索引.

    /** 3. 虚地址,穿插复制奇数和偶数索引
    addressMapping(i) = (1+2*(i)) % (n|1);
    Accessing A(0) actually accesses nums[1].
    Accessing A(1) actually accesses nums[3].
    Accessing A(2) actually accesses nums[5].
    Accessing A(3) actually accesses nums[7].
    Accessing A(4) actually accesses nums[9].
    Accessing A(5) actually accesses nums[0].
    Accessing A(6) actually accesses nums[2].
    Accessing A(7) actually accesses nums[4].
    Accessing A(8) actually accesses nums[6].
    Accessing A(9) actually accesses nums[8]. **/

int L = 0, R = 0, e = nums.length - 1;
        while(R <= e) {
            if(nums[addressMapping(R)] > k) {
                swap(addressMapping(R), addressMapping(L));
                L++;
                R++;
            } else if(nums[addressMapping(R)] < k) {
                swap(addressMapping(R), addressMapping(e));
                e--;
            } else R++;
}

 

 

ST 表 (Spare Table)

解决重复问题。比如求一个数之前 k 步的某个 值,最大值、最小值、可以用ST表。

1) st[i][j] = i ~ i + 2**j - 1.

2) st[i][j] = max(st[i][j-1], st[i+2**(j-1)][j-1])

3) maxium, l ~ r. 

4) x = floor ( log(r - l + 1))

5) res = max ( st[i][x], st[r+1-2**x][x])

 

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int [] res = new int[n-k+1];
        int m = log(k);
        int [][] st = new int[n][m+1];
        for(int i = 0; i < n; i++) st[i][0] = nums[i];
        for(int j = 1; j <= m; j++){
            for(int i = 0; i + (1 << (j-1)) < n; i++){
                int left = i + (1 << (j-1));
                st[i][j] = Math.max(st[i][j-1], st[left][j-1]);
            }
        }
        m--;
        for(int i = 0; i <= n-k; i++){
            // log(r-l+1), r - 2**x, x
            res[i] = Math.max(st[i][m], st[i+k-(1<<m)][m]);
        }
        return res;
    }

    public int log(int num){
        int res = 0;
        while((1<<res) < num){
            res++;
        }
        if(res == 0) res++;
        return res;
    }
}

 

两个有序集合维护前 x 大

  • Two TreeSet or TreeMap to solve this problem
  • Every time to move elements to the other.

 

Nim定理。Game Theory

一堆石子,异或值不为0,先手赢。pick out :  a - (a-a^x).

 

逆元

  • 除法的逆元不能直接用。 (a/b) % M = (a%M * inv(b%M))%M.
  • 其中 inv(x) * x = 1, 在 %M 的情况下。并且, 当 M是质数,如果gcd(x,M) = 1 则 x^(M-1) = 1. 因此 x * inv(x) = x^(M-1) , 则 inv(x) = x^(M-2). x^(M-2) 由快速幂算得。
    long qpow(long base, int n) {
        long ans = 1;
        while(n != 0) {
            if((n&1) == 1) {
                ans = ans * base % M;
            }
            n = n >> 1;
            base = base * base % M;
        }
        return ans;
    }
   invs[b%M] = qpow(b%M, M-2);
   res = res%M  * invs[b%M] % M; // (res / b) %M

 

10进制转K进制

public void ok(long num, int k){
        ArrayList<Integer> list = new ArrayList<>();
        int top = 0;
        long base = 1;
        while(base <= num){
            base *= k;
            top++;
        }
        top--;
        base /= k;
        while(top >= 0){
            if(base <= num){
                int mul = 1;
                while(mul * base <= num) mul++;
                mul--;
                list.add(mul);
                num -= base * mul;
            } else {
                list.add(0);
            }
            top--;
            base /= k;
        }
    }

 

求10进制数所有的palindrome

每次只求一半,根据回文长度,选择一半的长度。

int [][] table = new int[][]{
        {1, 9},
        {10, 99},
        {100, 999},
        {1000, 9999},
        {10000, 99999},
        {100000, 999999},
        {1000000, 9999999},
        {10000000, 99999999},
    };
    public long kMirror(int k, int n) {
        // string forward backward same k-based
        // max, len. add one, getnext, base, k
        // isTenBasedMirror() true add, false 
        long res = 0;
        int base = 0;
        int len = 1;
        int cnt = 0;
        int ind = 0;
        while(cnt < n){
            base++;
            if(base > table[ind][1]) {
                len++;
                ind = (len-1) / 2;
                base = table[ind][0];
            }

            long mirrorNumber = build(base, len);
            if(mirrorNumber % k == 0) continue;

            if(ok(mirrorNumber, k)){
                res += mirrorNumber;
                cnt++;
            }
        }
        return res;
    }

    public long build(long base, int len){
        List<Integer> list = new ArrayList<>();
        long num = base;
        while(num > 0){
            list.add((int)(num % 10));
            num /= 10;
        }
        if(len % 2 == 0){
            for(int i = 0; i < list.size(); i++){
                base *= 10;
                base += list.get(i);
            }
        }else {
            for(int i = 1; i < list.size(); i++){
                base *= 10;
                base += list.get(i);
            }
        }
        return base;
    }

  

其他经验总结:

1、树的遍历 根据树字符串的顺序还原二叉树。无递归遍历 
package algorithm;


import algorithm.objs.TreeNode;

import java.util.ArrayDeque;

public class TravelBinaryTree {
    //前序遍历,前,左,右

    public void preOrder(TreeNode node) {
        if(node == null) return;
        System.out.println(node.value);
        preOrder(node.left);
        preOrder(node.right);
    }

    //中序遍历 左 中 右
    public void inOrder(TreeNode node) {
        if(node == null) return;
        inOrder(node.left);
        System.out.println(node.value);
        inOrder(node.right);
    }

    //后序遍历   左  右 中
    public void postOrder(TreeNode node) {
        if(node == null) return;
        postOrder(node.left);
        postOrder(node.right);
        System.out.println(node.value);
    }

    public void preOrderWithoutRecursion(TreeNode node){
        //按顺序压入栈
        ArrayDeque<TreeNode> q = new ArrayDeque<>();
        q.push(node);
        while (!q.isEmpty()){
            TreeNode first = q.pop();
            System.out.println(first.value);
            if(first.right != null) q.push(first.right);
            if(first.left != null) q.push(first.left);
        }
    }

    public void inOrderWithoutRecursion(TreeNode node){
        ArrayDeque<TreeNode> q = new ArrayDeque<>();
        //pop 一次为中间节点
        q.push(node);
        while (!q.isEmpty()){
            TreeNode root = q.peek();
            while (root.left != null){
                TreeNode before = root;
                q.push(root.left);
                root = root.left;
                before.left = null;
            }

            TreeNode last = q.pop();
            System.out.println(last.value);

            if(last.right != null) q.push(last.right);
        }
    }

    public void postOrderWithoutRecursive(TreeNode node){
        ArrayDeque<TreeNode> q = new ArrayDeque<>();
        q.push(node);
        while (!q.isEmpty()){
            TreeNode current = q.pop();
            if(current.left == null && current.right == null){
                System.out.println(current.value);
            }else{
                q.push(current);
            }
            if(current.right != null) q.push(current.right);
            if(current.left != null) q.push(current.left);
            current.left = null;
            current.right =null;
        }
    }

    public int buildBinaryTreePreorderDfs(TreeNode node, String s, int index){
        if(index == s.length()) return index;
        if(s.charAt(index) == '-') return index+1;

        node.value = s.charAt(index) - '0';

        int ind = index+1;

        if(ind < s.length() && s.charAt(ind) != '-'){
            node.left = new TreeNode();
            ind = buildBinaryTreePreorderDfs(node.left, s, ind);
        }else if(ind < s.length()){
            ind++;
        }


        if(ind <  s.length() && s.charAt(ind) != '-'){
            node.right = new TreeNode();
            ind = buildBinaryTreePreorderDfs(node.right, s, ind);
        }else if(ind < s.length()){
            ind++;
        }

        return ind;
    }



    public TreeNode buildTreeFromStringPreOrder(){
        String s = "126-42--3---36--5--";
        TreeNode root = new TreeNode();
        buildBinaryTreePreorderDfs(root, s, 0);
        return root;
    }

    public TreeNode buildTreeFromStringInOrder(){
        String s = "-6-2-4-3--1-6-3-5-";
        TreeNode root = new TreeNode();
        buildBinaryTreePreorderDfs(root, s, 0);
        return root;
    }

    public TreeNode buildTreeFromStringPostOrder(){
        String s = "126-42--3---36--5--";
        TreeNode root = new TreeNode();
        buildBinaryTreePreorderDfs(root, s, 0);
        return root;
    }

//    public TreeNode buildTreeFromStringInOrder(){
//        String s = "126-42--3---36--5--";
//    }

    public static void main(String [] args){
        TravelBinaryTree travelBinaryTree = new TravelBinaryTree();
        TreeNode root = travelBinaryTree.buildTreeFromStringPreOrder();

//        travelBinaryTree.preOrder(root);

        travelBinaryTree.postOrderWithoutRecursive(root);
    }

}

 

2、二维矩阵和
  •  以0,0为左上,任意为右下。这个矩阵的和用dp可以计算。为 dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-1] + matrix[i][j]

  • x1, y1 左上, x2, y2右下的矩阵和 sum = dp[x2][y2] - dp[x2][y1] - dp[x1][y2] + dp[x1][y1]
  • 画图很直观
package algorithm;

public class SubmarixSum {
    public int submatrixSum(int [][] matrix, int x1, int y1, int x2, int y2){
        int res = 0, m = matrix.length, n = matrix[0].length;
        int [][] dp = new int[m+1][n+1];

        //多出一行一列位0的 空间位同行同列矩阵做处理
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n;j++){
                dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-1] + matrix[i-1][j-1];
            }
        }
        return dp[x2+1][y2+1] - dp[x1][y2+1] - dp[x2+1][y1] + dp[x1][y1];
    }
}
 3、维护一个TOP K数组考虑priorityQueue。如果遍历的数组有序,则用普通队列即可。
4、区间dp模板
1 for(int l = 1; l <= n; l++){ // l 长度
2             for(int i = 0; i < n; i++){  //i 起始点
3                 j = i+l-1;  // j 终点 
4                 for(int s = 1; s <= j; s++){ // s 中间点
5                     do(i~s-1). // 起始点~s-1 干点啥
6                     do(s~j)  // s ~ 终点干点啥
7                 }
8             }
9         }
View Code
5、向左找第一个遇到的最大或者最小。用一个队列维护。覆盖问题。
6、如果结果包含有限制,可以枚举所有值,或者所有组合。这样时间复杂度,是一个常数固定值。
7、枚举所有可能。可以用统计二进制的1的位数。则可以统计所有出现的可能。最大值用 1 << N 表示。
8 、数位DP。用一个mask。每一位表示成二进制。1表示访问过,0表示没访问过。主要DFS用来确定好上下限,例如给了一个low,一个high,每一位的值要在里面。其他的至于统计相同数还是不同数只是在里面做计算,没什么其他的。
9、求第k大子数组和。用一个堆维护。记录下标和当前和。向堆里面添加两个和。sum = sum_i + nums(i+1).  sum = sum_i - num_i + num(i+1). 可以证明一下可以得到所有的子序列和。 
  1. 设所有 i 为当前的数组下标。则前 i 的所有子序列组合设为 A(i-1)。 A(i-1) + nums[i] U A(i-1) 为 到 i 为止的所有子序列和 设为A(i)。
  2. 对于nums[i-1] 如何通过。怎么得到所有子序列和? A(i) U A(i) + nums[i]。
  3. 对于某一个子数组和 sum[i]. 则通过上述构建方式,构建所有子序列和:
  4. 一种是包含 nums[i] 的子序列和即 sum[i].  next = sum[i] + nums[i+1].
  5. 一种是nums[i]之前的,sum[i]-nums[i]. next = sum[i] - nums[i] + nums[i+1]
  6.  将4、5加入队列。即为新构建的子序列和。
  7. 上述过程实际上每一次对最后加入的nums[i] 的 序列和进行一次 分裂。
  8. 上述方式可以得到所有子序列的组合
10、判断一个下标是否在存在的范围段里面。可以用一个TreeSet维护(需要注意 lower 和 higher的null情况。)或者TreeMap。
11、meet in the middle算法。在要给集合里找固定长度的。可以将集合折半,然后分别从每一个集合里面找x、y。 x +y=target。对于第二个集合可以用二分加速。
12、构造时间长度和数组的成绩和,可以有小到大 一次增加 a0 + a1 + a2 + a3. 每一次后面加一数ai,然后一次把这个数加上。
13、两个有序数组求中位数。
  • 对短的数组用二分。
  • 判断是否加入这个数组多一些元素还是加入另一个数组多一些元素
  • 最后对 偶数数组、奇数数组分情况讨论。讨论时候加入边界思考
package ltc;

public class Q4{
    public static double findMedianSortedArrays(int[] A, int[] B) {
        int m = A.length, n = B.length;
        if(m > n) return findMedianSortedArrays(B, A);
        // A[am+1] < B[bm]  l = am+1
        // A[am] > B[bm+1] r = am-1
        if(m == 1 && n == 1) return (A[0]+B[0])/2.0;
        if(m == 0) {
            if(n%2 == 1) return B[n/2];
            else return (B[n/2-1] + B[n/2])/2.0;
        }
        int al = 0, ar = m-1, t = (m+n+1)/2;
        while (al <= ar){
            int am = (al+ar)/2;
            int bm = t-(am+1)-1;
            if(am >= 0 && am < m-1 && A[am+1] < B[bm]) al = am+1; // need more A-elements
            else if(bm >= 0 && bm < n-1 && A[am] > B[bm+1]) ar = am-1; // need more B-elements
            else{
                if(bm == -1) return (A[am] + B[0])/2.0;
                int l = 0, r= 0;
                if((m+n)%2 == 0){
                    l = Math.max(A[am], B[bm]);
                    if(am < m-1 && bm < n-1){
                        r = Math.min(A[am+1], B[bm+1]);
                    }else if (am == m-1 && bm < n-1){
                        r = B[bm+1];
                    }else if (bm == n-1 && am < m-1){
                        r = A[am+1];
                    }else{
                        r = Math.max(A[am], B[bm]);
                    }
                }else{
                    l = Math.max(A[am], B[bm]);
                    r = l;
                }
                return (l+r)/2.0;
            }
        }

        int l = 0, r = 0;
        if((m+n)%2==0){
            if(t == n){
                l = B[t-1];
                r = A[al];
            }else{
                r = B[t-1];
                l = Math.min(A[al], B[t]);
            }
        }else{
            l = B[t-1];
            r = l;
        }
        return (l+r)/2.0;
    }

    public static void main(String [] args){
        int [] A = {1,4};
        int [] B = {2,3,4,6}; // 3.5      1,2,3,4,4,6
        System.out.println(findMedianSortedArrays(A, B));
    }
}

 

完全二叉树的深度

用二进制表示的话,则为二进制的长度,root1, left = val*2, right = val*2+1;

公共祖先,相同深度后,a ^ (b>>d),为所差深度,向上移动a就可以。

Moore's Voting Algorithm

数组存在majority(超过数组一半的长度),用两两抵消,剩下的就是 majority

 

其他:

lc 复杂暂时不写题目直接跳过( 检查仔细性,写前想明白就好,这种想不明白不要写,要不然出现错误盲目去改会越来越乱):
  • 736 Lisp反解
  • 749 每次给一个最大区域加墙
  • LCP13
  • 420
  • 936
  • 782
  • 1157
  • 1862 (如果可以对商进行遍历会降低复杂度到 n * log(n), 例如 求 y 的个数满足,d = y/x, 可以枚举 x, d, )
  • 1521  (考虑 & 的性质,先从左向右遍历,再从右向左,如果&的值已经算过了,则不再) 
posted @ 2020-11-28 13:49  ylxn  阅读(245)  评论(0编辑  收藏  举报