刷题篇。树、图、数组等相关常见处理操作
一、树
1、遍历
2、寻找根节点到目标节点的路径
- 找到根节点到该节点的路径,反向标记根节点和父节点。
- 标记到目标节点的方向是左孩子,还是右孩子。
if(node.val == tartget){ backTrace.add(node); return true; } if(findTarget(nood.left, target)){ backTrace.add(node, true) return true; } ... right
3、重构二叉树(前序+中序)
- 第一个节点是前序遍历的根。
- 找中序遍历的节点
- 如果中序遍历左面有节点,则到根的部分是左子树
- 剩下的是右子树.
- 循环直到所有节点遍历完毕
class Solution { public TreeNode dfs(int [] p, int ps, int pe, int [] in, int is, int ie){ if(p.length == 0 || in.length == 0) return null; TreeNode root = new TreeNode(p[ps]); int is1 = is; for(; is1 <= ie; is1++){ if(in[is1] == p[ps]){ break; } } if(is1 > is){ root.left = dfs(p, ps+1, is1-is+ps, in, is, is1-1); } if(is1 < ie){ root.right = dfs(p, is1-is+ps+1, pe, in, is1+1, ie); } return root; } public TreeNode buildTree(int[] preorder, int[] inorder) { return dfs(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1); } }
4、重构二叉树(后序+中序)
- 后序遍历最后一个点为根节点
- 然后中序遍历找到该节点
- 中序遍历确定左右子树的长度
- 后序遍历根节点左侧个右子树长度为右子树
- 后续遍历0开始,右侧左子树个长度为左子树
5、跟据连通两点建树:
- 因为是树所以可以任意选一点当根节点
- bfs,从根开始往下建树,同时可以计算树的深度
6、跟据所有祖先、和孩子的关系,建树
-
因为给了所有的祖先孩子的关系,所有可以找到度这个衡量关系
- 度高的在上
- 度高的点包含所有子孩子的节点。
- 如果度相同,且两个点所包含的子节点和父节点相同,则可以护换,树的构建形式可以变换
- 如果度最接近的相连点不能交换,则度更高的祖先一定不能和当前点交换。
- 因此是否可以交换考虑最接近的点就可以
class Solution { //1 find all node with agjoint nodes. //2 find the biggest degrees as root. degress = node - 1; //3 traverse all node. if its father contains all its node. can build a tree. else return 0; //4 if partent's size == node's size. return 2. can build multiple trees. public int checkWays(int[][] pairs) { HashMap<Integer, HashSet<Integer>> edges = new HashMap<>(); for(int [] pair : pairs){ edges.putIfAbsent(pair[0], new HashSet<>()); edges.putIfAbsent(pair[1], new HashSet<>()); edges.get(pair[0]).add(pair[1]); edges.get(pair[1]).add(pair[0]); } int root = -1; for(Map.Entry<Integer, HashSet<Integer>> entry : edges.entrySet()){ if(entry.getValue().size() == edges.keySet().size()-1) root = entry.getKey(); } if(root == -1) return 0; int res = 1; for(Map.Entry<Integer, HashSet<Integer>> entry : edges.entrySet()){ int node = entry.getKey(); if(node == root) continue; int nodeSize = entry.getValue().size(); HashSet<Integer> connected = entry.getValue(); int nearestNode = -1; int nearestNodeSize = 0x7ffffff; //nearest node. for(int connectNode : connected){ int tempSize = edges.get(connectNode).size(); if(tempSize >= nodeSize && tempSize < nearestNodeSize){ nearestNodeSize = tempSize; nearestNode = connectNode; } } HashSet<Integer> nearestConnected = edges.get(nearestNode); for(int connectNode : connected){ if(connectNode == nearestNode) continue; if(!nearestConnected.contains(connectNode)) return 0; } if(nearestNodeSize == nodeSize) res = 2; } return res; } }
7、二叉树的序列化和反序列化。不使用临时变量。
序列化:
- 先序遍历增加字符
- 增加分隔符
- 如果孩子为空增加“null”
反序列化:
- 先序遍历反序列化
- 第一个节点是根,遇到null返回。
- 先遍历左孩子、再右孩子
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ public class Codec { public void dfsSer(TreeNode root, StringBuilder str){ if(root == null) { str.append("null,"); return; } str.append(root.val + ","); dfsSer(root.left, str); dfsSer(root.right, str); } public TreeNode dfsDecode(List<String> list){ if(list.size() == 0 || list.get(0).equals("null")) { if(list.size() != 0) list.remove(0); return null; } TreeNode node = new TreeNode(Integer.parseInt(list.get(0))); list.remove(0); node.left = dfsDecode(list); node.right = dfsDecode(list); return node; } // Encodes a tree to a single string. public String serialize(TreeNode root) { StringBuilder sb = new StringBuilder(); dfsSer(root, sb); return sb.toString(); } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { System.out.println(data); List<String> list = new LinkedList<>(); for(String str:data.split(",")) list.add(str); return dfsDecode(list); } }
8、树形DP。
- 找好父节点、子节点的转移方程。好像没说一样。
- 一般步骤,以0为根节点,再换根,依次算,res[cur] = res[father] + ....
9、任意子路径异或值
树以任意点为根节点,然后得到连续一段的路径的异或值(根异或A,截断点异或 B) 为 A XOR B。
10、判断节点A是不是节点B的祖先,可以跟据DFS性质。
- 用一个递增的时间戳。in[A] < in[B] < out[A] 则B在递归A的时候被访问到,即A是B的祖先。
二、数组
1、接雨水
- 木桶原理。一个桶能接多少水不取决于最高模板。而是取决于最低木板。因此此类问题先定义好木桶,然后沿着最低木板进行计算。
- 优先队列一直记录最低木板,然后一直沿着最低木板看是否能更新,也就是把他给填上
2、二分专项
- 确定好最大最小
- 确定好target。
- 确定好边界(这个需要仔细,往往这里会错)
常见target:
1)第k大,需要数数目,跟据二分的结果看找到的数目是小于k还是大于等于k,判断左右指针的移动方向。
2)直接给定target 直接二分。
3)最小好进制(给定位数形式,反推target),从最多位到小遍历,然后最小进制位2,最多位num-1,跟据满足形式位数形式判断 移动 L 还是 R
4)
3、背包专项
1)01背包。
- 注意更新滚动数组时候由大到小更新。如果由小到大更新,则可能刚更新的值,在本轮又进行了计算(新值应该在下一轮计算,否则导致重复拿,变成了完全背包)。
相关题目:
粉碎石头。
- 每次挑两个石头去粉碎,求最后剩的石头最小的可能值。最后结果为 SUM(ki*nums[i]]),ki为 +1 或者 -1。例如,第i个石头要么先碎,那么ki = 1,要么后碎,那么ki = -1.
- 最后的结果为 abs( pos - neg) 最小。则为 abs(pos - (sum - pos)) 最小 ====> abs(sum - 2pos) 最小 =====》 2*POS 上限为 sum 的 最大值。
2)完全背包
- dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v).
- 滚动数组计算方式跟0、1背包相反(由小到大更新,则代表可以重复拿当前元素)
3) 多重背包
- 每个物品m个。到target是否能构成。
可以判断最后到target的个数是否还能剩余m个。其他和完全背包计算顺序相同。一直去滚动剩余数目数组。
4)多重背包 2。求最大价值
- 每个物品m个。总类别 N 2000, 总体积 V 2000,总m2000.
N * V * m 朴素贝叶斯算会TLE。二进制优化。把m个物品打包。
正确性讨论:
- 小二进制组合一定可以形成大数。
- 例如当计算 (J为体积)f[j],f[j] = max(f[j], f[j-v] + s, f[j-2v] + 2s..... ). 如果 f[j-iv] + iw > f[j] 即选了 i 个当前物品的值会 使得体积j的包价值最大,那么 f[j-(i-1)v] + (i-1)w 一定大于 f[j-(i-1)v]. 反证法。
5)多重背包 3。求最大价值
- 每个物品m个。总类别20000,总体积20000。
如果用4)的解法依然会超时。
- 当体积为 j 时候。一共由 j % v 种情况
- 其中每一种情况为 dp[j] = max(dp[j], dp[j+v] + w, dp[j + 2v] + 2w + .... ) 直到 j + kv <= m.
- dp[j + iv] ,只与前面的 s * v 体积的最大值有关。用一个滑动窗口(单调递减队列)去维护这个最大值即可
- dp[j] = max( dp[j-v] + w, dp[j-2v] + 2w....) 还有个自身比较 dp[j]
- dp[j+v] = max( dp[j] + w, dp[j-v] + w2, dp[j-2v] + 2w ...) 还有个自身比较 dp[j+v]
- 因此由小到大计算的化,只需要维护一个大小为 i的窗口即可。i*k <= m . 窗口用单调递减队列表示。
4、统计一个数组里面前有多少数比后面大的组合对数。
1) 可以用归并排序。merge的时候左右两部分因为都是有序的,因此可以统计。
2) 类似的只有统计两个值,右边比左边大、小或者是在某个范围的数目,都可以用归并排序。
5、重叠问题与区间计算
常用贪心 或者线段树解决。
1)线段树
- 常用来求区域和、区域最值
- 一种是固定区间线段树,还有一种是动态开点线段树
动态开点求和线段树
1 package algorithm; 2 3 public class SegmentTreeSum { 4 class Node{ 5 int ls, rs; 6 int val, add; 7 } 8 9 int N = (int)1e9, M = 120010, cnt = 1; 10 Node [] tr = new Node[M]; 11 12 void pushDown(int u, int len){ 13 tr[tr[u].ls].add += tr[u].add; 14 tr[tr[u].rs].add += tr[u].add; 15 tr[tr[u].ls].val += (len-len/2)*tr[u].add; 16 tr[tr[u].rs].val += len/2 * tr[u].add; 17 tr[u].add = 0; 18 } 19 20 void pushUp(int u){ 21 tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val; 22 } 23 24 void lazyCreate(int u){ 25 if(tr[u] == null) tr[u] = new Node(); 26 if(tr[u].ls == 0){ 27 tr[u].ls = ++cnt; 28 tr[tr[u].ls] = new Node(); 29 } 30 if(tr[u].rs == 0){ 31 tr[u].rs = ++cnt; 32 tr[tr[u].rs] = new Node(); 33 } 34 } 35 36 void update(int u, int lc, int rc, int L, int R, int v){ 37 if(L <= lc && rc <= R){ 38 tr[u].val += (rc - lc + 1) * v; 39 tr[u].add += v; 40 return; 41 } 42 43 lazyCreate(u); 44 pushDown(u, rc-lc+1); 45 int mid = (lc + rc)/2; 46 if(L <= mid) update(tr[u].ls, lc, mid, L, R, v); 47 if(R > mid) update(tr[u].rs, mid+1, rc, L, R, v); 48 pushUp(u); 49 } 50 51 int query(int u, int lc, int rc, int L, int R){ 52 if(L <= lc && rc <= R) return tr[u].val; 53 lazyCreate(u); 54 pushDown(u, rc-lc+1); 55 int mid = (lc+rc)/2, ans = 0; 56 if(L <= mid) ans = query(tr[u].ls, lc, mid, L, R); 57 else ans += query(tr[u].rs, mid + 1, rc, L, R); 58 return ans; 59 } 60 }
动态开点最大值计算线段树
1 package algorithm; 2 3 import java.util.List; 4 5 public class SegmentTreeMax { 6 int N = (int)1e9; 7 class Node{ 8 Node ls, rs; 9 int val, add; 10 } 11 Node root = new Node(); 12 void pushDown(Node node){ 13 if(node.ls == null) node.ls = new Node(); 14 if(node.rs == null) node.rs = new Node(); 15 if(node.add == 0) return; 16 node.ls.add = node.add; 17 node.rs.add = node.add; 18 node.ls.val = node.add; 19 node.rs.val = node.add; 20 } 21 22 void pushUp(Node node){ 23 node.val = Math.max(node.ls.val, node.rs.val); 24 } 25 void update(Node node, int lc, int rc, int L, int R, int v){ 26 if(L <= lc && rc <= R){ 27 node.add = v; 28 node.val = v; 29 return; 30 } 31 pushDown(node); 32 int mid = (lc + rc) /2; 33 if(L <= mid) update(node.ls, lc, mid, L, R, v); 34 if(R > mid) update(node.rs, mid+1, rc, L, R, v); 35 pushUp(node); 36 } 37 }
6、最终递增子序列 LIC
解法1):dp。o(n**2)
解法2):二分。(这个要学一下,不然最优解不会写可还行。。。)
- 维护一个长为 len的数组。索引为长度,值为最小末尾数值。
- 根据贪心,如果长为len,我要找最长的LIS,我要使 d[len] 尽可能小,这样才能让后面的值能加进来
- 因此如果nums[i] > d[len] 则 d[len+1] = nums[i]
- 如果 nums[i] <= d[len] 则二分找位置l,d[l-1] < nums[i] < d[l] 更新 d[l] 为 nums[i]. 则长度为l 的结尾元素变小。
- 用此过程动态维护 所有长度的 序列的末尾值,一直边小。
1 public int lengthOfLIS(int[] nums) { 2 //LIS 3 int n= nums.length; 4 int [] d= new int[n+1]; 5 int len = 1; 6 d[len] = nums[0]; 7 for(int i = 1; i < n; i++){ 8 if(d[len] < nums[i]){ 9 len++; 10 d[len] = nums[i]; 11 }else{ 12 int L = 1, R = len; 13 while(L <= R){ 14 int M = (L+R)/2; 15 if(d[M] >= nums[i]) R = M-1; 16 else L = M + 1; 17 } 18 d[L] = nums[i]; 19 } 20 } 21 return len; 22 }
其他:
1)
- 有i个物品,每种ai个。取相同物品为一种取法。不同物品为不同取法。一共有多少种取法? dp[i+1][j] = sum( dp[i][j-k] ). k ~ 0 to min(j, ai).
- sum( dp[i][j-k] ). k ~ 0 to min(j, ai). = sum( dp[i][j-1-k] ) + dp[i][j] - dp[i][j-1-a]
- 变形后为 dp[i+1][j] = dp[i+1][j-1] + dp[i][j] - dp[i][j-1-a]
- 复杂度 由 O(mn*2) 下降到 O(mn) 。
2)商旅问题。
问题描述:从0点出发,然后经过所有的点回到0,求权值和最小的路径。
解法:状态压缩DP。
- 转移方程:dp[S][v] = min(dp[S U u][u] + d(u, v))
- 这里面S U u 不是整数,但是可以用位数来表示集合。
3)
三、字符串
1) 字符串匹配
- KMP
- BM
- RABIN-KARP
2)回文
- 一个字符串自身最大回文长度(必须s的起点打头)。KMP算法。回文有属性。a1~ai = ai ~ a1. 所以用KMP把 s 先当作模板,然后 反转s,去和s匹配。最后的输出 j 即为匹配的长度。
四、数学计算
1)最大公约数
- gcd(a,b) = gcd(b, a%b)。
- 退出条件 b == 0
2) 扩展欧几里得算法
- ax + by = gcd(a, b)
- 然后跟据 gcd (a, b) = gcd(b, a%b) 不断求出一组组解
3)素数
- 算到 根号n即可
4)素数的个数
- 从最小的算。然后 < n 的所有基数的倍数 全部删除即可
5)快速幂运算(反复平方法)
LAST、其他变换
- n的阶乘后面0的个数。n里面含有5阶乘的个数。n <= 5*zero(num).
- 一个数组的&值会越来越小。如果数值范围为 2**31,那么最多有31个1,所以所有的数&值最多只有 32种(还有一个全0,因为有单调性)
- 单调队列优化动态规划。例如 DP[I] 只依赖于 DP[I-K] ~ DP[I-1] 中的最大值,可以用一个单调递减队列存入窗口的值,然后每次取最大的计算,并更新窗口。
五、图
1 求强连通分量、割点、桥。Tarjan算法
tarjan(u){ DFN[u] = Low[u] =++Index Stack.push(u) for each(u, v) in E if(v is not visited) // 没被访问继续找 tarjan(v) Low[u] = min(Low[u], Low[v]) else if (v in S) // 在栈内 Low[u] = min(Low[u], DFN[v]) if(DFN[u] == Low[u]) repeat v = S.pop print v until (u == v) }
谢谢!