每日算法刷题Day79:10.25:leetcode 一般树5道题,用时1h30min
四、有递有归
1.套路
2.题目描述
3.学习经验
1. 3593. 使叶子路径成本相等的最小增量(中等)
3593. 使叶子路径成本相等的最小增量 - 力扣(LeetCode)
思想
1.给你一个整数 n,以及一个无向树,该树以节点 0 为根节点,包含 n 个节点,节点编号从 0 到 n - 1。这棵树由一个长度为 n - 1 的二维数组 edges 表示,其中 edges[i] = [ui, vi] 表示节点 ui 和节点 vi 之间存在一条边。
每个节点 i 都有一个关联的成本 cost[i],表示经过该节点的成本。
路径得分 定义为路径上所有节点成本的总和。
你的目标是通过给任意数量的节点 增加 成本(可以增加任意非负值),使得所有从根节点到叶子节点的路径得分 相等 。
返回需要增加成本的节点数的 最小值 。
2.算法思想:贪心地让一个子树的根节点想另一子树更大的根节点对齐,而不改变子树内其他节点的值。
对于当前节点,遍历所有孩子得到其到叶子的路径成本,并记录最大值与小于最大值的个数,让所有孩子向最大的路径成本对齐,即小于最大值的个数就是需要增加成本的节点数,从而让下层路径成本一致
代码
class Solution {
public:typedef long long ll;vector<vector<int>> g;ll res = 0;ll dfs(int cur, int fa, vector<int>& cost) {ll totalCost = cost[cur];ll maxn = LLONG_MIN;ll cnt = 0;ll cntNxt = 0;for (auto& nxt : g[cur]) {if (nxt == fa)continue;ll nxtCost = dfs(nxt, cur, cost);if (nxtCost < maxn) {++cnt;} else if (nxtCost > maxn) {maxn = nxtCost;cnt = cntNxt;}++cntNxt;}res += cnt;if (maxn != LLONG_MIN)totalCost += maxn;return totalCost;}int minIncrease(int n, vector<vector<int>>& edges, vector<int>& cost) {g.resize(n);for (auto& e : edges) {int a = e[0], b = e[1];g[a].push_back(b);g[b].push_back(a);}dfs(0, -1, cost);return res;}
};
五、树的直径
1.套路
2.题目描述
3.学习经验
1. 2246.相邻字符不同的最长路径(困难,学习,题目看错了,是能做的)
2246. 相邻字符不同的最长路径 - 力扣(LeetCode)
思想
1.给你一棵 树(即一个连通、无向、无环图),根节点是节点 0 ,这棵树由编号从 0 到 n - 1 的 n 个节点组成。用下标从 0 开始、长度为 n 的数组 parent 来表示这棵树,其中 parent[i] 是节点 i 的父节点,由于节点 0 是根节点,所以 parent[0] == -1 。
另给你一个字符串 s ,长度也是 n ,其中 s[i] 表示分配给节点 i 的字符。
请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径 ,并返回该路径的长度。
2.题目说是"任意一对相邻节点",而不是任意一对节点(特别难),所以还是树的直径,递归返回子节点子树的一条最大路径长度,然后在一个递归里得到两条最大链的长度,再加上判断"任意一对相邻节点"即可
3.递归输入参数不变的变量写引用,不然多次递归拷贝会超出内存限制
4.证明可以得到最大链和次大链:
res = max(res, maxLen + nxtLen);
maxLen = max(maxLen, nxtLen);
- (1)若maxLen>nxtLen:则为最大值和次大值
- (2)若maxLen<nxtLen:则为次大值和最大值
代码
class Solution {
public:vector<vector<int>> g;int n;int res = INT_MIN;int dfs(int cur, string& s) {int maxLen = 0;for (auto& nxt : g[cur]) {int nxtLen = dfs(nxt, s) + 1;if (s[cur] != s[nxt]) {res = max(res, maxLen + nxtLen);maxLen = max(maxLen, nxtLen);}}return maxLen;}int longestPath(vector<int>& parent, string s) {n = parent.size();g.resize(n);int root;for (int i = 0; i < n; ++i) {if (parent[i] == -1)root = i;elseg[parent[i]].push_back(i);}dfs(root, s);if (res == INT_MIN)return 1;return res + 1; // 节点数}
};
六、树的拓扑排序
1.套路
2.题目描述
3.学习经验
1. 310.最小高度树(中等,学习两种方法)
310. 最小高度树 - 力扣(LeetCode)
思想
1.树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,任何一个没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
2.首先记住一个结论:
设 dist[x][y] 表示从节点 x 到节点 y 的距离,假设树中距离最长的两个节点为 (x,y),它们之间的距离为 maxdist=dist[x][y],则可以推出以任意节点构成的树最小高度一定为 minheight=⌈maxdist/2⌉(最长距离取中间为根节点,平分高度),且最小高度的树根节点一定在节点 x 到节点 y 的路径上(中间节点为根节点)。
假设最长的路径的 m 个节点依次为 p1→p2→⋯→pm,最长路径的长度为 m−1,可以得到以下结论:
- 如果 m 为偶数,此时最小高度树的根节点为 pm/2或者 p(m/2+1),且此时最小的高度为m/2;
- 如果 m 为奇数,此时最小高度树的根节点为 p((m+1)/2且此时最小的高度为m−1/2。
3.所以此题可以有两种解法:
(1)利用换根技巧找到距离最长的两个节点 (x,y)与它们之间的路径 - 以任意节点 p 出现,利用广度优先搜索或者深度优先搜索找到以 p 为起点的最长路径的终点 x;
- 以节点 x 出发,找到以 x 为起点的最长路径的终点 y;
- x 到 y 之间的路径即为图中的最长路径,找到路径的中间节点即为根节点。
(2)利用拓扑排序思想:
假设最长距离的两个节点x 到 y 的路径为 x→p1→p2→⋯→pk−1→pk→y,根据方法一的证明已知最小树的根节点一定为该路径中的中间节点,我们尝试删除最外层的度为 1(不区分入度和出度,无向图就是边数) 的节点 x,y 后,则可以知道路径中与 x,y 相邻的节点 p1,pk此时也变为度为 1 的节点,此时我们再次删除最外层度为 1 的节点直到剩下根节点为止。
具体做法如下: - 首先找到所有度为 1 的节点压入队列,此时令节点剩余计数 remainNodes=n;
- 同时将当前 remainNodes 计数减去出度为 1 的节点数目,将最外层的度为 1 的叶子节点取出,并将与之相邻的节点的度减少,重复上述步骤将当前节点中度为 1 的节点压入队列中;
- 重复上述步骤,直到剩余的节点数组 remainNodes≤2 时,此时剩余的节点即为当前高度最小树的根节点。
但此解法有个前提条件:节点度数>=1,但若只有一个根节点,应该输出0,需提前单独判断
代码
超时遍历:
class Solution {
public:vector<vector<int>> g;int dfs(int cur, int fa) {int maxH = 0;for (auto& nxt : g[cur]) {if (nxt == fa)continue;int nxtH = dfs(nxt, cur) + 1;maxH = max(maxH, nxtH);}return maxH;}vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {g.resize(n);for (auto& e : edges) {int a = e[0], b = e[1];g[a].push_back(b);g[b].push_back(a);}vector<int> tmp(n);int minH = INT_MAX;for (int i = 0; i < n; ++i) {tmp[i] = dfs(i, -1);minH = min(minH, tmp[i]);}vector<int> res;for (int i = 0; i < n; ++i) {if (tmp[i] == minH)res.push_back(i);}return res;}
};
DFS换根:
class Solution {
public:vector<vector<int>> g;void dfs(int cur, vector<int>& dist, vector<int>& parent) {for (auto& nxt : g[cur]) {if (dist[nxt] < 0) {dist[nxt] = dist[cur] + 1;parent[nxt] = cur;dfs(nxt, dist, parent);}}}int findLongestNode(int n, int root, vector<int>& parent) {vector<int> dist(n, -1); // 距离dist[root] = 0;dfs(root, dist, parent);int maxDist = 0;int tarNode = -1;for (int i = 0; i < n; ++i) {if (dist[i] > maxDist) {maxDist = dist[i];tarNode = i;}}return tarNode;}vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {if (n == 1)return {0};g.resize(n);for (auto& e : edges) {int a = e[0], b = e[1];g[a].push_back(b);g[b].push_back(a);}vector<int> parent(n, -1); // 记录路径int x = findLongestNode(n, 0, parent); // 从0找xint y = findLongestNode(n, x, parent); // 从x找y// x->y,从y反向追溯路径vector<int> path;parent[x] = -1;while (y != -1) {path.push_back(y);y = parent[y];}int len = path.size();if (len % 2)return {path[len / 2]}; // 路径长度为奇数elsereturn {path[len / 2 - 1], path[len / 2]}; // 路径长度为偶数}
};
拓扑排序:
class Solution {
public:vector<vector<int>> g;vector<int> deg;vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {if(n==1) return {0};g.resize(n);deg.assign(n, 0);for (auto& e : edges) {int a = e[0], b = e[1];g[a].push_back(b);g[b].push_back(a);++deg[a];++deg[b];}queue<int> que;for (int i = 0; i < n; ++i) {if (deg[i] == 1)que.push(i);}int cnt = n;while (cnt > 2) {// 一次性弹出当前度为1的节点int qSz = que.size();cnt -= qSz;for (int i = 0; i < qSz; ++i) {int x = que.front();que.pop();for (auto& y : g[x]) {// x-y--deg[y];if (deg[y] == 1)que.push(y);}}}vector<int> res;while (!que.empty()) {int x = que.front();que.pop();res.push_back(x);}return res;}
};
七、DFS时间戳
1.套路
1.可以把树上问题转化成数组问题,比如子树的操作转化成子数组的操作。通常要结合其他数据结构。
2.题目描述
3.学习经验
1. 3515.带权树中的最短路径(困难,待学习,先过)
3515. 带权树中的最短路径 - 力扣(LeetCode)
思想
代码
八、最近公共祖先(LCA)、倍增算法
1.套路
1.带权树 LCA 模板(节点编号从 0 开始):
typedef long long ll;
typedef pair<int,int> PII;
class LcaBinaryLifting{vector<int> depth; // 每个节点的深度// 如果是无权树(边权为1),dis可以去掉,用depth代替vector<ll> dis; // 从根节点到每个节点的距离vector<vector<int>> pa; // 倍增数组:pa[i][x]表示节点x向上跳2^i步后到达的祖先public:// 计算n的二进制长度int bit_width(unsigned n){ // unsigned确保逻辑右移int len=0;while(n){n>>=1;++len;}return len;}// 计算尾部零个数int count_zero(unsigned n){int cnt=0;while( (n&1)==0 && n!=0){ // n结尾是0且n不是0n>>=1;++cnt;}return cnt;}// 预处理depth,dis数组,初始化pa数组void dfs(int cur,int fa){pa[0][x]=fa; // fa->xfor(auto& nxt:g[cur]){int nxtNode=nxt.first;if(nxt==fa) continue;int nxtW=nxt.second;depth[nxtNode]=depth[cur]+1;dis[nxtNode]=dis[cur]+nxtW;dfs(nxtNode,cur);}}// 预处理depth,dis,pa数组LcaBinaryLifting(vector<vector<int>>& edges){int n=edges.size()+1;int m=bit_width((unsigned)n); // 需要m层倍增数组,m=log2n(向上取整),让2^(m-1)<n<=2^mvector<vector<PII>> g(n);for(auto& e:edges){// 从0开始,若从1开始,改为e[0]-1int x=e[0],y=e[1],w=e[2];g[x].push_back({y,w});g[y].push_back({x,w});}depth.resize(n,0);dis.resize(n,0);pa.resize(m,vector<int>(n,-1)); // -1表示不可达dfs(0,-1); // 目的:预处理depth,dis数组,初始化pa数组// 预处理倍增数组for(int i=0;i+1<m;++i){ for(int x=0;x<n;++x){if(pa[i][x]!=-1){ // 有祖先,别忘记判断pa[i+1][x]=pa[i][pa[i][x]]; // 跳2^(i+1)步 = 跳2^i步两次}}}}// 查找node的第k个祖先int get_kth_ancestor(int node,int k){// 将k分解成二进制表示,理解:(1)countr_zero((unsigned)k找到最低位1的位数(2)k&=k-1剪去相应位数的1(...10...0&...01...1=...00...0)for(;k>0 && node>=0;k&=k-1){ // 记得判断node>=0,防止为-1,数组越界node=pa[count_zero((unsigned)k)][node];}return node;/* 例子解释:1. k=13=1101countr_zero(1101)=0, 所以node向上跳2^0=1步2. k & k-1 = 1101 & 1100 = 1100 = 12(剪去第0位1)countr_zero(1100)=2, 所以node向上跳2^2=4步3. k & k-1 = 1100 & 1011 = 1000 = 8(剪去第2位1)countr_zero(1000)=3, 所以node向上跳2^3=8步4. k & k-1 = 1000 & 0111 = 0000 = 0 结束(剪去第3位1)即 13=1+4+8=2^0+2^2+2^3/*}// 查找x和y的最近公共祖先int get_lca(int x,int y){// 1.确保depth[x]<=depth[y]if(depth[x]>depth[y]) swap(x,y);// 2.将y提升到与x相同深度y = get_kth_ancestor(y,depth[y]-depth[x]);// 3.x是y的祖先if(y==x) return x;// 4.x和y同时向上跳(先跳大距离再跳小距离),直到遇到相同父节点for(int i=pa.size()-1;i>=0;--i){if(pa[i][x]!=pa[i][y]){ // 因为对齐过了,如果跳不了2^i,则都是-1,不会进入ifx=pa[i][x];y=pa[i][y];}}return pa[0][x]; // 最后跳1步,得到公共父节点}// x和y之间的路径距离:根节点到x距离+根节点到y距离-根节点到x和y最近公共祖先距离*2ll get_dis(int x,int y){return dis[x]+dis[y]-dis[get_lca(x,y)]*2;}
}
2.题目描述
3.学习经验
1. 1483. 树节点的第K个祖先(困难,模版题,一些注意点)
1483. 树节点的第 K 个祖先 - 力扣(LeetCode)
思想
1.给你一棵树,树上有 n 个节点,按从 0 到 n-1 编号。树以父节点数组的形式给出,其中 parent[i] 是节点 i 的父节点。树的根节点是编号为 0 的节点。
树节点的第 k 个祖先节点是从该节点到根节点路径上的第 k 个节点。
实现 TreeAncestor 类:
TreeAncestor(int n, int[] parent)对树和父数组中的节点数初始化对象。getKthAncestor``(int node, int k)返回节点node的第k个祖先节点。如果不存在这样的祖先节点,返回-1。
2.注意两个点:
(1)if (pa[i][x] != -1)有祖先才更新,因为会出现pa[i+1][pa[i][x]],为-1数组会越界
(2)k > 0 && node >= 0,因为会出现pa[count_zero((unsigned)k)][node],node为-1数组会越界
代码
class TreeAncestor {
public:vector<vector<int>> pa;int n, m;int bit_width(unsigned n) {int len = 0;while (n) {++len;n >>= 1;}return len;}TreeAncestor(int n, vector<int>& parent) {n = n;m = bit_width(((unsigned)n));pa.resize(m, vector<int>(n, -1));for (int i = 0; i < n; ++i) {pa[0][i] = parent[i];}for (int i = 0; i + 1 < m; ++i) {for (int x = 0; x < n; ++x) {if (pa[i][x] != -1)pa[i + 1][x] = pa[i][pa[i][x]];}}}int count_zero(unsigned x) {int cnt = 0;while ((x & 1) == 0 && x != 0) {++cnt;x >>= 1;}return cnt;}int getKthAncestor(int node, int k) {for (; k > 0 && node >= 0; k &= k - 1) {node = pa[count_zero((unsigned)k)][node];}return node;}
};/*** Your TreeAncestor object will be instantiated and called as such:* TreeAncestor* obj = new TreeAncestor(n, parent);* int param_1 = obj->getKthAncestor(node,k);*/
2. 3559.给边赋权值的方案数II(困难,学习结论)
3559. 给边赋权值的方案数 II - 力扣(LeetCode)
思想
1.给你一棵有 n 个节点的无向树,节点从 1 到 n 编号,树以节点 1 为根。树由一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示在节点 ui 和 vi 之间有一条边。
一开始,所有边的权重为 0。你可以将每条边的权重设为 1 或 2。
两个节点 u 和 v 之间路径的 代价 是连接它们路径上所有边的权重之和。
给定一个二维整数数组 queries。对于每个 queries[i] = [ui, vi],计算从节点 ui 到 vi 的路径中,使得路径代价为 奇数 的权重分配方式数量。
返回一个数组 answer,其中 answer[i] 表示第 i 个查询的合法赋值方式数量。
由于答案可能很大,请对每个 answer[i] 取模 109 + 7。
注意: 对于每个查询,仅考虑 ui 到 vi 路径上的边,忽略其他边。
2.本质是用LCA模版求(x,y)路径长度len,然后从 len 个不同元素中,选奇数个数(选奇数条边设置边权为 1),有 2^(len−1) 种选法(结论)。
3.但是直接写:
if (len == 0)return 0;
return 1LL << (len - 1);
会溢出,所以可以预处理2的幂数组,同时对1e9+7取模:
vector<ll> pow2;
const int mod = 1e9 + 7;
const int maxn = 1e5;
void init_pow2() {pow2.resize(maxn);pow2[0] = 1;for (int i = 1; i < maxn; ++i) {pow2[i] = pow2[i - 1] * 2 % mod;}
}
// ...
if (len == 0)return 0;
return pow2[len - 1];
代码
有几个错误样例:
class Solution {
public:typedef long long ll;int n, m;vector<vector<int>> g, pa;vector<int> dep;int bit_width(unsigned n) {int len = 0;while (n) {++len;n >>= 1;}return len;}void dfs(int cur, int fa) {pa[0][cur] = fa;for (auto& nxt : g[cur]) {if (nxt == fa)continue;dep[nxt] = dep[cur] + 1;dfs(nxt, cur);}}int count_zero(unsigned k) {int cnt = 0;while ((k & 1) == 0 && k > 0) {++cnt;k >>= 1;}return cnt;}int get_kth_ancestor(int x, int k) {for (; k > 0 && x >= 0; k &= k - 1) {x = pa[count_zero((unsigned)k)][x];}return x;}int get_lca(int x, int y) {if (dep[x] > dep[y])swap(x, y);y = get_kth_ancestor(y, dep[y] - dep[x]);if (y == x)return x;for (int i = m - 1; i >= 0; --i) {if (pa[i][x] != pa[i][y]) {x = pa[i][x];y = pa[i][y];}}return pa[0][x];}ll get_res(int x, int y) {int len = dep[x] + dep[y] - dep[get_lca(x, y)] * 2;if (len == 0)return 0;return 1LL << (len - 1);}vector<int> assignEdgeWeights(vector<vector<int>>& edges,vector<vector<int>>& queries) {n = edges.size() + 1;m = bit_width((unsigned)n);g.resize(n);pa.resize(m, vector<int>(n, -1));dep.resize(n, 0);for (auto& e : edges) {int u = e[0] - 1, v = e[1] - 1;g[u].push_back(v);g[v].push_back(u);}dfs(0, -1);for (int i = 0; i + 1 < m; ++i) {for (int x = 0; x < n; ++x) {if (pa[i][x] != -1)pa[i + 1][x] = pa[i][pa[i][x]];}}vector<int> res;for (auto& q : queries) {res.push_back(get_res(q[0] - 1, q[1] - 1));}return res;}
};
预处理2的幂数组并取模:
vector<ll> pow2;
const int mod = 1e9 + 7;
const int maxn = 1e5;
void init_pow2() {pow2.resize(maxn);pow2[0] = 1;for (int i = 1; i < maxn; ++i) {pow2[i] = pow2[i - 1] * 2 % mod;}
}
// ...
if (len == 0)return 0;
return pow2[len - 1];
