@LINK: https://leetcode.cn/problems/collect-coins-in-a-tree/
;
题解
有两个方法, 第一种方法是常规的思维定式的 也比较复杂, 第二种方法要简易的多 还需要思维跳跃;
所有节点分为两种: 标记点(必须要扫描到他) 和 未标记点;
方法1: 树上DFS
很容易想到的一个方法是: 遍历每一个节点, 得到当前节点的步数, 然后和Ans
取一个最小值;
于是问题就转换为: 对于一个节点, 如何求其步数;
任意节点 他的路径上 任一经过的边, 一定是经过了2
次 (即去一次 回一次); 换句话说, 他所有经过的边 会构成一棵树; 也就是, 他是原图的一个子树(连通子图);
.
这一点非常非常重要, 这是该方法的基础; 证明一下(1 因为是一笔画问题, 所以其所涉及的所有边 构成一个连通子图 即子树) (2 对于该子树上的任一边 一定是一去一回 最优策略不会经过多次; 即总步数为: 该子树的边数 * 2
)
根据此, 将该子树(路径) 分成2个部分; 我们以0为树根, 该子树分为2个部分, 当前节点为c
其父节点为f
, 子树的一部分为c-f-...
从c到f往上的这部分 记作Step_up[]
, 另部分是c往下到各个儿子这部分 往下的这部分我们记作为Step_down[]
; (该节点的答案为: Step_up[c] + Step_down[c]
)
--
计算Step_down
令D1_down[x]
为: 以0为树根, x
的所有为标记点的子节点(包括自己)中 最深的深度depth
;
当前节点c的一个儿子s 如果c需要前往s (即c-s这条边在子树中)
⟺
\iff
⟺ D1_down[s] - depth[c] > 2
;
.
这一判定条件, 非常非常重要, 只要证明此转换公式是正确的 一切都变的简单; 否则, 如果你设置D1: 距离为1的标记点; D2: 距离为2的标记点; D3: 距离>2的标记点
这样问题会变得很复杂;
.
如果c需要前往s, 则更新Step_down[ c] += (2 + Step_down[ s])
; 这个更新公式也非常重要;
--
计算Step_up
void Dfs_up( int _cur, int _fa, int _step_up, int _maxLen_up, int _deep){
_step_up
即为答案, 即cur需要往上走的子树的步数 (答案为_step_up + Step_down[cur]
);
_maxLen_up
为: 除去cur子树(即cur及其子节点)后的子树 (也就是cur往上经过fa的所有节点)中的所有标记点 到cur的最大距离 (注意, 不包括cur
);
.
有两个取值: (1 -1
表示这个没有满足要求的标记点) (2 >=1
表示存在符合要求的标记点 表示距离cur的最大距离);
.
只有当_maxLen_up > 2
时, 此时_step_up
一定是>= 2
的;
_deep
为当前节点的深度;
这个更新也很复杂, 对于dfs_up
你的关注 重点, 要放到当cur -> s
往儿子走时 如何确保儿子s
的信息是正确的;
s
是否需要前往cur
呢? 这和一样, 如果s
的maxLen_up > 2
, 则s
必须前往cur
;
.
但更新公式不是step_up[s] += (2 + step_up[cur])
这是错误的! step_up[cur]
是cur-fa的子树, 而还有一种情况 cur-s2
cur前往其他儿子(非s
) 对于s
来说 也属于up
往上;
.
即令ss
为cur的所有除了s
的儿子, s
往上的子树 其实是分为2个部分的 (1 step_up[cur]
) (2 cur
到ss
的Step_down
之和 也就是去除Step_down[s] + 2
后的Step_down[cur]
)
因此, 儿子s
的maxLen_up
和其step_up
一样, 也是分为两个情况 (一个是cur - fa
的部分) (一个是cur - ss
的部分);
这确实比较复杂;
代码
vector< int> * AA;
int D1_down[ 30004], D2_down[ 30004];
int Step_down[ 30004];
Graph * G;
int Ans;
void Dfs_down( int _cur, int _fa, int _deep){
vector< int> depth( 0);
auto add_depth = [&depth]( int _a){
depth.push_back( _a);
if( depth.size() > 2){
nth_element( depth.begin(), depth.begin() + 2, depth.end(), greater<>());
depth.resize( 2);
}
};
//--
Step_down[ _cur] = 0;
//--
for( int nex, e = G->Head[ _cur]; ~e; e = G->Next[ e]){
nex = G->Vertex[ e];
if( nex == _fa){ continue;}
Dfs_down( nex, _cur, _deep + 1);
//--
add_depth( D1_down[ nex]); // 没有D2[nex];
//--
if( D1_down[ nex] - _deep > 2){ Step_down[ _cur] += 2 + Step_down[ nex];}
}
//--
if( (* AA)[ _cur] == 1){ add_depth( _deep);}
while( depth.size() < 2){ add_depth( -1);}
sort( depth.begin(), depth.end(), greater<>());
D1_down[ _cur] = depth.front(), D2_down[ _cur] = depth.back();
//--
// D_( _cur S_ D1_down[ _cur] S_ D2_down[ _cur] S_ Step_down[ _cur]);
}
void Dfs_up( int _cur, int _fa, int _step_up, int _maxLen_up, int _deep){
// D_( _cur S_ _step_up S_ _maxLen_up);
Ans = min( Ans, _step_up + Step_down[ _cur]);
ASSERT_( _maxLen_up == -1 || _maxLen_up >= 1);
//--
if( _maxLen_up != -1){ ++ _maxLen_up;}
for( int nex, e = G->Head[ _cur]; ~e; e = G->Next[ e]){
nex = G->Vertex[ e];
if( nex == _fa){ continue;}
//--
int maxLen_down;
if( D1_down[ nex] != D1_down[ _cur]){ maxLen_down = D1_down[ _cur];}
else{ maxLen_down = D2_down[ _cur];}
if( maxLen_down != -1){
maxLen_down -= _deep;
++ maxLen_down;
}
//--
auto len = max( _maxLen_up, maxLen_down);
if( len > 2){
auto step_down = Step_down[ _cur];
if( D1_down[ nex] - _deep > 2){ step_down -= (2 + Step_down[ nex]);}
//--
Dfs_up( nex, _cur, (_step_up + step_down) + 2, len, _deep + 1);
}
else{
Dfs_up( nex, _cur, 0, len, _deep + 1);
}
}
}
int collectTheCoins(vector<int>& A, vector<vector<int>>& B) {
AA = &A;
int n = A.size();
G = new Graph( n, n * 2, n);
for( auto & v : B){ G->Add_edge( v[0], v[1]), G->Add_edge( v[1], v[0]);}
//--
Ans = 0x7F7F7F7F;
Dfs_down( 0, -1, 0);
Dfs_up( 0, -1, 0, -1, 0);
//--
return Ans;
}
方法2: 动态删除叶节点
这很需要思维跳跃性;
对于这棵树中的一个非标记点的叶节点c
, 将其和邻近边一同删除掉, 不停的重复次过程; (注意, 因为是动态的过程 我们所删去的节点 可能一开始并不是叶节点);
对于所删除的节点c
:
1 他一定可以不是答案 (即答案可以选择其他节点来获得)
.
当要删除c时 此时的子树中 c是叶子, 令f
为c
的邻接点 (只有一个), 对于最初树 f也是c邻接点 但c可能还有若干其他邻接点 (只是已经删去了) 令sub
为c的所有除了f
的子树集合 (即sub里的点和边 此时都已经删去了) ;
.
(1 如果c的答案路径为空, 则f的路径也为空)
.
(2 如果c的路径不为空 他一定不会经过sub
因为sub
都不是标记点, 即c的路径 一定是往f方向走的; 假设步数为x
, 则f的路径步数为x - 2
比c更优); 因此c一定可以不是答案;
2 答案节点的路径, 一定不经过该节点;
.
因为sub
不会是答案, 所以答案路径一定是从f
来到达c
, 而sub
里没有标记点 所以从f到c是无意义的;
--
此时删去完后, 此时的树 所有的叶节点 都是标记点;
令L
为所有的叶节点集合, U
为L
的邻接点集合, 现在不是动态的了, 一次性删除完L, U
, 剩下的树: 任一节点都是答案节点, 他们的路径都一样 都是这个剩下的树的边数 * 2
;
其实这个思路是错误的, 看个例子: X - b - a - Y, a - c - d - Z
(XYZ
为叶子 都是标记点)
.
对于Z
他的邻接点d
, 确实要删去, d
不会是答案 也不会被答案路径所经过;
.
但是, 重要的a
他是Y
的临界点, 但不可以去掉的, 因为a
距离X
为2; 换句话说, 最终的答案 是a-c
这个子树 (不管答案选a/c
, 路径都是这个子树, 即答案为: 这个子树的边数 * 2
)
正确的处理是: 分两次处理 (1 把所有叶节点给去掉) (2 再把所有叶节点给去掉)
代码
int collectTheCoins(vector<int>& A, vector<vector<int>>& B) {
if( accumulate( A.begin(), A.end(), 0) <= 1){ return 0;}
//--
int n = A.size();
Graph G( n, n * 2, n);
vector< int> Deg( n, 0);
for( auto & v : B){
G.Add_edge( v[0], v[1]), G.Add_edge( v[1], v[0]);
Deg[ v[0]] ++, Deg[ v[1]] ++;
}
//--
{ // 动态的 删除特定的叶子
queue< int> que;
for( int i = 0; i < n; ++i){
if( Deg[ i] == 1 && A[ i] == 0){ que.push( i);}
}
while( !que.empty()){
int cur = que.front(); que.pop();
ASSERT_( Deg[ cur] != 0);
if( Deg[ cur] == 0){ continue;}
for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){
nex = G.Vertex[ e];
//--
-- Deg[ cur], -- Deg[ nex];
if( Deg[ nex] == 1 && A[ nex] == 0){ que.push( nex);} // A[nex] == 1
}
}
}
{ // (现在叶子全是标记点) 删除所有叶子和`与叶子距离为1的新叶节点`;
{ // 第一层的叶子
queue< int> que;
for( int i = 0; i < n; ++i){
if( Deg[ i] == 1){
ASSERT_( A[ i] == 1);
que.push( i);
}
}
while( !que.empty()){
int cur = que.front(); que.pop();
if( Deg[ cur] == 0){ continue;}
for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){
nex = G.Vertex[ e];
//--
-- Deg[ cur], -- Deg[ nex];
}
}
}
{ // 第二层的叶子 (去除第一层叶子后 新的叶子)
queue< int> que;
for( int i = 0; i < n; ++i){
if( Deg[ i] == 1){
que.push( i);
}
}
while( !que.empty()){
int cur = que.front(); que.pop();
if( Deg[ cur] == 0){ continue;}
for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){
nex = G.Vertex[ e];
//--
-- Deg[ cur], -- Deg[ nex];
}
}
}
}
int Ans = 0;
for( int cur = 0; cur < n; ++cur){
for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){
nex = G.Vertex[ e];
if( Deg[ cur] > 0 && Deg[ nex] > 0){
++ Ans;
}
}
}
return Ans;
}