【算法学习笔记】48.递归/显式栈 SJTU OJ 1358 分割树

Description

现在有一棵树T,有N个节点,我们想通过去掉一个节点p来把T分割成更小的树,并且满足每个小树中的节点数不超过n/2。

请根据输入的树来输出所有可能的p的号码。

Input Format

第1行:一个整数N,代表有N个节点,且每个节点的编号为1,2,...,N。

第2~N行:每行两个整数x,y,代表节点x和节点y之间连通。

Output Format

从小到大一次输出满足条件的p的号码,每行1个可行解。

Input Sample

10
1 2
2 3
3 4
4 5
6 7
7 8
8 9
9 10
3 8

Output Sample

3
8


想法其实很简单...
首先用邻接表存储整个图(注意, n个点 n-1个边的图除了二叉树有很多种可能.....)
然后 把每个点的每个分支的节点数计算出来 最后进行判断输出即可
//递归的描述很简单
遍历所有的点,不断的递归调用getComponent函数即可,指定源头和分支首元素。
代码如下(没用Vector):
#include <iostream>
#define MaxN 10000+10
using namespace std;
 
int n;
int connect[MaxN][200]={0}; 
int len_c[MaxN]={0};
int seg[MaxN][200]={0}; 
bool caled[MaxN] = {0};  

void init(){
    cin>>n;
    //记录连通情况 O(n)
    for (int i = 0; i < n-1; ++i)
    {
        int x,y;
        cin>>x>>y;
        connect[x][len_c[x]++] = y;
        connect[y][len_c[y]++] = x;
    }
    //初始化所有只有一个分支的点的分割情况 
    for (int i = 1; i <= n; ++i) if(len_c[i]==1)
    {
        seg[i][0] = n-1;
        caled[i] = true;
    }
    //初始化有两个分支 且至少有一个分支只有一个元素的节点的分割情况
    for (int i = 1; i <= n; ++i) if(len_c[i]==2)
    {
        if(len_c[connect[i][0]]==1){
            seg[i][0] = 1;
            seg[i][1] = n-2;
            caled[i] = true;
        }
        if(len_c[connect[i][1]]==1){
            seg[i][1] = 1;
            seg[i][0] = n-2;
            caled[i] = true;
        }
        
    }
    //
}
 
 
//计算与s相连的 以x开头的那一个分支有多少个节点
int getComponent(int s, int x){
    int len = len_c[x];
    int res = 1;//肯定有x本身    
    if(caled[x]){
        for (int i = 0; i < len; ++i) if(connect[x][i]!=s)
            res += seg[x][i];
    }else{//说明x的seg情况没有初始化过 
        int tmp = 0;
        for (int i = 0; i < len-1; ++i)
        { 
            seg[x][i] = getComponent(x,connect[x][i]);
            tmp += seg[x][i];
            if(connect[x][i]!=s)
                res += seg[x][i];
        }
        seg[x][len-1] = n-1-tmp;
        caled[x] = true;
    }
    return res;
} 
 
void Build(){
    //去构建所有的seg
    for (int i = 1; i <= n; ++i) if(!caled[i])
    { 
        int len = len_c[i];
        int tmp = 0;
        for (int j = 0; j < len-1; ++j){
            //递归计算 i的以j开头的那个分支有多少个元素
            seg[i][j] = getComponent(i,connect[i][j]);
            tmp += seg[i][j];
        }
        seg[i][len-1] = n-1-tmp;//最后一个分支可以用补集的思想
        caled[i] = true;
    }
}
 
void Output(){
    for (int i = 1; i <= n; ++i)
    {
        bool ok = true;
        for (int j = 0; j < 3; ++j) if(seg[i][j]>n/2)
        {
            ok = false;
            break;
        }
        if(ok)
            cout<<i<<endl;
    }
}
 
int main(int argc, char const *argv[])
{
    init(); 
    Build();
    Output(); 
 
    return 0;
}
递归

//显式栈的算法描述比较复杂 如下:
1.预处理边界点和与边界点相邻的只有两个分支的点,这些点直接处理完成。
2.找到id最小的没有处理的点,入栈。
3.当栈非空的时候:
  取栈顶元素,看是否可以根据已有的情况处理它
  如果可以,把它处理,抛出栈
  如果不可以,把处理它需要先处理的那些节点找到,入栈。结束循环
这里要注意的一点就是不要让栈里有重复元素,否则会不断拉长。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <stack>
#include <vector>

#define MaxN 100010
using namespace std;

int n;// 节点个数

//int connect[MaxN][10]={0};//connect[i][0,1,2] 分别存储i节点的三个分支的首元素
//int seg[MaxN][MaxN]={0};//seg[i][j] 表示i节点的第j个分支的元素个数
//int** connect;
//int** seg;
vector<int> connect[MaxN];
vector<int> seg[MaxN];
int len[MaxN]={0};//len[i] 表示i节点的分支的个数
bool solved[MaxN] = {false};//表示是否已经计算完i的所有分支含有节点数目
bool inStack[MaxN] = {false};
int tobe_solved[MaxN] = {0};  //还没进行处理的分支的列表

stack<int> s;
void Init(){
    cin>>n;

    //动态申请空间
    

    //记录连通情况 O(n)
    for (int i = 0; i < n-1; ++i)
    {
        int x,y;
        scanf("%d %d",&x,&y);
        //connect[x][len[x]++] = y;
        //connect[y][len[y]++] = x;
        connect[x].push_back(y);
        connect[y].push_back(x);
        seg[x].push_back(0);
        seg[y].push_back(0);
    }
    


    //初始化 O(n) 以下两种情况可以直接搞定
    for (int i = 1; i <= n; ++i)
    {
        if(connect[i].size()==1){
            // 所有只有一个分支的点的分割情况
            seg[i][0] = n-1;
            solved[i] = true;
        }else if(connect[i].size()==2){
            // 有两个分支 且至少有一个分支只有一个元素的节点的分割情况
            if(connect[connect[i][0]].size()==1){
                seg[i][0] = 1;
                seg[i][1] = n-2;
                solved[i] = true;
            }
            if(connect[connect[i][1]].size()==1){
                seg[i][1] = 1;
                seg[i][0] = n-2;
                solved[i] = true;
            }
            
        }
    }
}

void Output(){
    //判断并输出 O(n)
    for (int i = 1; i <= n; ++i)
    {
        bool ok = true;//标志i点是否符合条件
        for (int j = 0; j < connect[i].size(); ++j) if(seg[i][j]>n/2)
        {//如果存在某一个分支的节点数目>n/2
            ok = false;
            break;
        }
        if(ok)
            printf("%d
",i);
    } 
    return;
}




void Build(){
    
    int cur = 1;//记录
    //把第一个还没有处理的元素加入栈
    for (; cur <= n; ++cur) if(!solved[cur])
    {
        s.push(cur);
        inStack[cur] = true; 
        break;
    }
    
    while(!s.empty()){
        int x = s.top();//待研究元素
        
        if(solved[x]){//如果该元素已经处理过了直接弹出
            s.pop();
            inStack[x] = false;
        }else{
            //如果x已经可以处理 则处理并抛出
            //否则 把需要先进行处理的元素入栈
            
            int have_solved = 0; //已经处理过的分支的个数
            int have_count = 0;  //已经处理过的分支的节点总数 用于算另外一个
            
            int tobe_solved_len = 0; //还没进行处理的分支的个数
            
            //开始研究所有的分支 常数循环
            for (int i = 0; i < connect[x].size(); ++i)
            {
                if(solved[connect[x][i]]){//如果这个分支的开头元素被处理过了
                    int toGet = connect[x][i];
                    //找到在toGet的分支中 非x的其他分支的和 + 自己 = tmp
                    int tmp = 1;
                    for (int j = 0; j < connect[toGet].size(); ++j)
                        if(connect[toGet][j]!=x)
                            tmp += seg[toGet][j];
                    have_count += tmp; //已经处理过的分支的节点总数累加
                    seg[x][i] = tmp;   //x的第i个分支处理过了
                    have_solved++;       //处理过的分支数加一
                }else{
                    //存储没有解决的分支的id
                    tobe_solved[tobe_solved_len++] = i;
                }
            }
            if(tobe_solved_len==1){ //只有一个分支还没进行处理 可以用补集
                seg[x][tobe_solved[0]] = n - 1 - have_count;
                solved[x] = true;
                inStack[x] = false;
                s.pop();
            }else if(tobe_solved_len==0){//周围的分支都处理过了 说明它也处理完成
                solved[x] = true;
                inStack[x] = false;
                s.pop();
            }else{//暂时不能进行处理 则把应该先处理的点加入栈
                for (int k = 0; k < tobe_solved_len; ++k){
                    int todo = connect[x][tobe_solved[k]];
                    if(inStack[todo]==false){//todo如果不在栈里 才有入栈的必要 否则会把栈拉长
                        s.push(todo);
                        inStack[todo] = true;
                    }
                }
            } 
        }
        
        if(s.empty()){
            //有可能还有没处理过的点 找到id最小的加入
            for (; cur <= n; ++cur) if(!solved[cur])
            {
                s.push(cur);
                inStack[cur] = true;
                break;
            }
        }
    }
}


int main(int argc, char const *argv[])
{
    Init();
    Build();
    Output(); 
    
    return 0;
}

/*
13
1 3
3 2
3 4
2 5
2 6
4 7
4 8
5 9
6 12
10 11
12 13
9 10
 */
显式栈

因为数据量太大了 必须用vector 才可以解决 (PS:如果动态申请数组的过程中,超过了内存限制 那么返回的是RE)

显式栈代码更复杂 但是应该更快一些。


还有一个算法就是,不断从边界点向内部冲洗,更新。用的是BFS+队列的思想,像洪水一样不断累积。
代码如下:
#include <iostream>
#include <queue>
 
#define MaxN 100000
using namespace std;
 
int n;
int connect[MaxN][3]={0};
int len_c[MaxN]={0};
int seg[MaxN][3]={0};
bool seg_ed[MaxN][3] = {false};
bool caled[MaxN] = {0};
 
struct Component
{
    int source;
    int to;
    int count;
};
 
 
void init(){
    cin>>n;
    //记录连通情况 O(n)
    for (int i = 0; i < n-1; ++i)
    {
        int x,y;
        cin>>x>>y;
        connect[x][len_c[x]++] = y;
        connect[y][len_c[y]++] = x;
    }
    //初始化所有只有一个分支的点的分割情况
//    for (int i = 1; i <= n; ++i) if(len_c[i]==1)
//    {
//        seg[i][0] = n-1;//唯一的一个分支有n-1个点
//        caled[i] = true;//计算过
//    }
    // //初始化所有 有两个分支 且 至少有一个分支只有一个元素的节点的分割情况
    // for (int i = 1; i <= n; ++i) if(len_c[i]==2)
    // {
    //     if(len_c[connect[i][0]]==1){ //0号分支是边界点
    //         seg[i][0] = 1;
    //         seg[i][1] = n-2;
    //     }
    //     if(len_c[connect[i][1]]==1){ //1号分支是边界点
    //         seg[i][1] = 1;
    //         seg[i][0] = n-2;
    //     }
    //     caled[i] = true; //计算过
    // }
    // //
    
}
 
 
 
//计算与s相连的 以x开头的那一个分支有多少个
int getComponent(int s, int x){
    int len = len_c[x];//len是该分支的子分支的个数
    int res = 1;//该分支的总数里肯定有x本身    所以初始化为1
    if(caled[x]){//如果这个分支是计算过的 就直接返回结果
        for (int i = 0; i < len; ++i)
            if(connect[x][i]!=s) //排除和s接通的那个子分支
                res += seg[x][i];
    }else{//说明x的seg情况没有初始化过
        int tmp = 0;
        for (int i = 0; i < len; ++i)
        {
            seg[x][i] = getComponent(x,connect[x][i]);
            tmp += seg[x][i];
            if(connect[x][i]!=s)
                res += seg[x][i];
        }
        // seg[x][len-1] = n-1-tmp;
        // if(connect[x][len-1]!=s)
        //         res += seg[x][len-1];
        caled[x] = true;
    }
    return res;
}
 
// void VisibleBuild(){
//     stack<int> s;
//     s.push(1);
//     while(!s.empty()){
 
//     }
// }
 
 
//计算每个点的每个分支的节点个数
void Build(){
    
    for (int i = 1; i <= n; ++i) if(!caled[i]) //用caled来减少重复计算
    {
        int len = len_c[i]; //len是当前节点的分支的数目
        int tmp = 0; //记录除了最后一个分支的节点数目和 因为最后一个节点可以用补集来算
        for (int j = 0; j < len-1; ++j){
            //递归计算 i的第j个分支的节点数目
            seg[i][j] = getComponent(i,connect[i][j]);
            tmp += seg[i][j];
        }
        seg[i][len-1] = n-1-tmp;//补集的思想 最后一个分治的节点数目就是n-1-前几个分支的节点总数
        caled[i] = true;
    }
}
 
 
void Output(){
    //判断并输出 O(n)
    for (int i = 1; i <= n; ++i)
    {
        bool ok = true;//标志i点是否符合条件
        for (int j = 0; j < len_c[i]; ++j) if(seg[i][j]>n/2)
        {//如果存在某一个分支的节点数目>n/2
            ok = false;
            break;
        }
        if(ok)
            cout<<i<<endl;
    }
    return;
}
 
 
void NewBuild(){
    //以所有的边界点为切入点进入
    for (int i = 1; i <= n; ++i) if(len_c[i]==1)
    {
        //printf("new start %d",i);
        queue<Component> q;
        Component start;
        start.source = i;
        start.to = connect[i][0];
        start.count = 1;
        q.push(start);//把它紧连着的那个分支的根压入
      //  bool wrong = false;
        while(!q.empty()){
            Component todo =  q.front();
            q.pop();
            int newStart = todo.to;
            for (int j = 0; j < len_c[newStart]; ++j)
            {
                if(connect[newStart][j]==todo.source){
                    seg[newStart][j] += todo.count;
//                    printf("-----
%d : from %d :%d	 from %d :%d	from %d :%d
",newStart,connect[newStart][0],seg[newStart][0],connect[newStart][1],seg[newStart][1],connect[newStart][2],seg[newStart][2]);
                }else{
                    Component next;
                    next.source = newStart;
                    next.to = connect[newStart][j];
                    int cur = 0;
                    for(;cur<len_c[next.to];++cur) if(connect[next.to][cur]==newStart)
                        break;
                    next.count = seg[next.to][cur]==0 ? todo.count + 1 : todo.count;
                    q.push(next);
                }
            }
//            printf("segment states:
");
//            for (int i = 1; i <= n; ++i)
//            {
//                printf("%d : from %d :%d	 from %d :%d	from %d :%d
",i,connect[i][0],seg[i][0],connect[i][1],seg[i][1],connect[i][2],seg[i][2]);
//            }
//            
//            wrong = false;
        }
    }
}
 
int main(int argc, char const *argv[])
{
    init();      //输入和一些边界情况的初始化
    //Build();  //计算每个点的每个分支的节点个数
    NewBuild();
    
    Output(); //遍历每个点判断条件并输出结果
    
    // printf("connect states:
");
    // for (int i = 1; i <= n; ++i)
    // {
    //     printf("%d : len:%d c0:%d	c1:%d	c2:%d
",i,len_c[i],connect[i][0],connect[i][1],connect[i][2]);
    // }
    // printf("segment states:
");
    // for (int i = 1; i <= n; ++i)
    // {
    //     printf("%d : seg0:%d	seg1:%d	seg2:%d
",i,seg[i][0],seg[i][1],seg[i][2]);
    // }
    return 0;
}
 
/*
 //n个点 n-1个边 一定是个二叉树
10
3 4
4 5
6 7
7 8
2 1
2 3
8 9
9 10
3 8
 
 */
队列

这个有很多错误的地方 一个是分支数上限的设置。 另外就是不知道为何队列会很长很长 超过时间限制(如果用数组手写队列的话就是RE)。

原文地址:https://www.cnblogs.com/yuchenlin/p/sjtu_oj_1358.html