刷题篇。树、图、数组等相关常见处理操作

一、树

 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个物品打包。

正确性讨论:

  1. 小二进制组合一定可以形成大数。
  2. 例如当计算 (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)的解法依然会超时。

  1. 当体积为 j 时候。一共由 j % v 种情况
  2. 其中每一种情况为 dp[j] = max(dp[j], dp[j+v] + w, dp[j + 2v] + 2w + .... ) 直到 j + kv <= m. 
  3. dp[j + iv] ,只与前面的 s * v 体积的最大值有关。用一个滑动窗口(单调递减队列)去维护这个最大值即可
  4. dp[j] = max( dp[j-v] + w, dp[j-2v] + 2w....) 还有个自身比较 dp[j]
  5. dp[j+v] = max( dp[j] + w, dp[j-v] + w2, dp[j-2v] + 2w ...) 还有个自身比较 dp[j+v] 
  6. 因此由小到大计算的化,只需要维护一个大小为 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) 

  1. 有i个物品,每种ai个。取相同物品为一种取法。不同物品为不同取法。一共有多少种取法? dp[i+1][j] = sum( dp[i][j-k] ). k ~ 0 to min(j, ai). 
  2. 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]
  3. 变形后为  dp[i+1][j] = dp[i+1][j-1] + dp[i][j] - dp[i][j-1-a]
  4. 复杂度 由   O(mn*2) 下降到 O(mn) 。 

 2)商旅问题。

问题描述:从0点出发,然后经过所有的点回到0,求权值和最小的路径。

解法:状态压缩DP。

  1. 转移方程:dp[S][v] = min(dp[S U u][u] + d(u, v))
  2. 这里面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、其他变换

  1. n的阶乘后面0的个数。n里面含有5阶乘的个数。n <= 5*zero(num).
  2. 一个数组的&值会越来越小。如果数值范围为 2**31,那么最多有31个1,所以所有的数&值最多只有 32种(还有一个全0,因为有单调性)
  3. 单调队列优化动态规划。例如 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)
        }

 

posted @ 2022-08-29 10:31  ylxn  阅读(39)  评论(0编辑  收藏  举报