題解/算法 {2603. 收集树中金币}

@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呢? 这和一样, 如果smaxLen_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 curssStep_down之和 也就是去除Step_down[s] + 2后的Step_down[cur])
因此, 儿子smaxLen_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是叶子, 令fc的邻接点 (只有一个), 对于最初树 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为所有的叶节点集合, UL的邻接点集合, 现在不是动态的了, 一次性删除完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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值