《算法导论第二版》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模板
步骤
- 找最下左点,当作极点。
- 跟据到极点的极角,由小到大排,如果极角相同按照距离由小到大排列。crossProduct(a, b) = a.x*b.y - b.x*a.y. (如果a在b的逆时针方向,crossProduct < 0)
- 排序后的点,如果最后一个极角对应多个点,则交换上面所有点的顺序,按照到距离由大到小排列(上一步是由小到大)。
- 按照对点的排序顺序放入堆 前2 个点。从第三个点开始进行下一个步骤
- 遍历排序的点
- 标记堆的top与top-1为 a ,b,当前遍历的点为c
- 判断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 树状数组
- 频繁单点插入
- 频繁区间求和
- 用binaryindexedtree.
- update. x += (x&-x)
- preSum x -= (x&-x)
- 1 index start
- 最低位 lowbit = x & (-x)
- 父节点 x + lowbit(x)
- 前一个根结点 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-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
- 将最优化问题转化为:对其做选择后,可只剩下一个子问题需要求解
- 做出贪心选择后,原问题总存在最优解,即贪心选择总是安全的
- 做出贪心选择后,剩余的子问题满足:其最优解与贪心选择组合可得到原问题最优解,这样就得到最优子结构
- 活动策划问题。动态规划到贪心的流程
- 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树
- 了解磁盘工作过程
- 了解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 松弛操作性质:
- 最短路径满足三角不等式原理
- 距离是最短路径的上界
- 非路径性质
- 路径松弛性质: (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) . 则就能得到最短路径。
- 前驱子图性质
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
5 差分系统的约束解。要会证明,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 }
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). 可以证明一下可以得到所有的子序列和。
- 设所有 i 为当前的数组下标。则前 i 的所有子序列组合设为 A(i-1)。 A(i-1) + nums[i] U A(i-1) 为 到 i 为止的所有子序列和 设为A(i)。
- 对于nums[i-1] 如何通过。怎么得到所有子序列和? A(i) U A(i) + nums[i]。
- 对于某一个子数组和 sum[i]. 则通过上述构建方式,构建所有子序列和:
- 一种是包含 nums[i] 的子序列和即 sum[i]. next = sum[i] + nums[i+1].
- 一种是nums[i]之前的,sum[i]-nums[i]. next = sum[i] - nums[i] + nums[i+1]
- 将4、5加入队列。即为新构建的子序列和。
- 上述过程实际上每一次对最后加入的nums[i] 的 序列和进行一次 分裂。
- 上述方式可以得到所有子序列的组合
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 (考虑 & 的性质,先从左向右遍历,再从右向左,如果&的值已经算过了,则不再)