【良心树】这个问题的代码超乎想象的简洁

【问题描述】

      给定一颗有根树,顶点编号为1~n,树是一个无环的连通图,有根树有一个特定的顶点,称为根。

      顶点 i 的祖先是从根到顶点 i 的路径上除顶点 i 以外的所有顶点,顶点 i 的父母是 i 的祖先中最接近 i 的顶点,每个顶点都是它父母的孩子。在给定的树中,顶点 i 的父母是顶点pi,对于根,pi为-1。例如:

      这是一个n=8个顶点的树,根为5, 顶点2的父母为3,顶点1的父母为5,6的祖先为4和5,7的祖先为8、3和5。

      在树中,其中一些顶点不尊重其他一些顶点,实际上,如果ci =1,表示顶点 i 不尊重它的所有祖先,而如果ci =0,则表示它尊重它所有的祖先。

      你需要一个一个地删除一些顶点,在每一步中,选择一个非根顶点,它不尊重它的父母并且它的所有孩子顶点也不尊重它。如果有几个这样的顶点,你需要选择具有最小编号的顶点。当你删除了这样的一个顶点v , 的所有子顶点与的父母顶点相连。

        上图是删除顶点7的示例。

         直到树中无满足删除标准的顶点,则上述过程停止。按顺序输出你删除的所有顶点,注意这个顺序的唯一的。

【输入形式】

         输入的第一行为一个整数 n (1≤ n ≤105),表示树的顶点数。

         接下来的 n 行描述了整颗树:第 i 行包含两个整数 pi  ci (1≤ pi ≤ n,  0≤  ci ≤1),这里 pi 是顶点i 的父母,若ci=0,表示顶点 i 尊重它的父母,ci=1,表示顶点 i 不尊重它的父母,pi=-1时,表示顶点 i 是树的根,同时 ci=0。

【输出形式】

         如果树中至少有一个顶点被删除,则按照顺序输出顶点编号,否则输入-1。

【样例输入1】

5

3 1

1 1

-1 0

2 1

3 0

【样例输出1】

1 2 4

【样例输入2】

5

-1 0

1 1

1 1

2 0

3 0

【样例输出2】

-1

【样例输入3】

8

2 1

-1 0

1 0

1 1

1 1

4 0

5 1

7 0

【样例输出3】

5

【样例说明】
第一个样例的删除过程如下(在图中,ci=1的顶点是黄色的)

  • 首先删除顶点1,因为它不尊重祖先并且它的所有孩子也不尊重它,而1是这样的顶点中编号最小的
  • 删除后顶点2将连接到顶点3
  • 然后删除顶点2,因为它不尊重祖先并且它的所有孩子也不尊重它。
  • 顶点4将连接到顶点3
  • 然后删除顶点4,因为它不尊重祖先,并且它的所有孩子也不尊重它(无孩子)
  • 无更多顶点可删

在第二个样例中,无需删除顶点

  • 顶点2和3的孩子尊重它们
  • 顶点4和5尊重它们的祖先

在第三个样例中显示如下

PS 题目里面的“尊重”太拗口了,这里用染色,反正都差不多

我这里记录一下做这道题的心路历程吧,一开始,我肯定是觉得字很多,挺难的,后面又想了一下,发现这是一个模拟删除的过程,就是说:一个节点如果被上色并且它的全部子节点也被上色,这个节点要被删除,它的子节点会被连接到这个子节点的父节点上。这个思路颇为简单直接,代码实现上是不是很像链表的删除哈哈哈,难点是数据的存储吧,因为数据量很大,还有一个父节点的问题,我们希望可以直接访问到子节点的父亲节点,这好办啊,我们直接搞一个数组parent[n],其中parent[i]表示第i个元素对应的父节点,它的存入就是输入的时候直接存进去就可以了,其实把数据存进去,这个模拟过程也就差不多完成了。

by the way(这里有点偏题),这里顺带说一下自己的感想,我们在课程上学了那么多具体的算法啊,模板啊之类的,一定不能死死地套用,我们新的知识是为这道题服务的,我们不能学了邻接表就直接套用算法书上面的模板,而是要有自己的思考,我要如何存储,才能更合适地服务于这道题是吧。其实我们只要想清楚到时候我们要如何使用这些信息,我们就知道待会怎么存储这个数据了,其实这里还是有点类似于哈希表的一个思想吧,就是数学里面的一一映射关系,也有点类似于桶排序里面的桶,因为这种访问方式的时间复杂度是最快的,通常是O(1),这是一种很重要的思想,很多题都会用到,我们之前做题的时候也是有意无意会用到,所以说,这个东西还是好用的啊。他要是节点的数值给得特别复杂,那就真得写散列函数还有线性探查函数了,这不是这道题考察的重点,就不说了。其实更多的我们会用stl里面的map吧,也是键值对,它是一棵红黑树,嘎嘎好用的,不会写也可以用,它的调用应该是O(logn)的。总之能优化就优化,尽量避免去循环的找某一个目标元素。

我们就是先遍历一遍,把待删节点标记,这里很简单,就是遍历一遍它的子节点们,看看是不是都被染色了,然后把要删除的节点加入到一个队列里面,为啥要这样呢?因为,我们只是对于每一个父节点的子节点进行了遍历嘛,如代码所示,我们还需要继续去看看,如果去掉了当前节点以后,子节点接上去了,子节点的子节点会不会因此受到影响,也要被删去呢?最后判断是不是当前节点的父节点也要被删去。最后就可以输出了。

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> parent(n, -1);
    vector<int> respect(n);
    vector<vector<int>> children(n);
    vector<bool> canDelete(n, false);
    queue<int> toDelete;
    vector<int> result;

    // Read input
    int root = -1;
    for (int i = 0; i < n; ++i) {
        int p, c;
        cin >> p >> c;
        if (p == -1) {
            root = i;
        } else {
            p--; // Convert 1-based to 0-based
            parent[i] = p;
            children[p].push_back(i);
        }
        respect[i] = c;
    }

    // Determine initial deletable nodes
    for (int i = 0; i < n; ++i) {
        if (i != root && respect[i] == 1) {
            bool allRespect = true;
            for (int child : children[i]) {
                if (respect[child] == 0) {
                    allRespect = false;
                    break;
                }
            }
            if (allRespect) {
                toDelete.push(i);
                canDelete[i] = true;
            }
        }
    }

    // Process deletable nodes
    while (!toDelete.empty()) {
        int node = toDelete.front(); toDelete.pop();
        result.push_back(node + 1); // Convert back to 1-based for output
        int par = parent[node];

        // Redirect children of the deleted node to its parent
        for (int child : children[node]) {//遍历的是待删除节点的孩子节点 
            if (parent[node] != -1) {
                children[par].push_back(child);//这是父节点的子节点,就是那个待删除结点的兄弟姐妹,就是要加入 
                parent[child] = par;

                // Recheck if this child can now be deleted
                if (respect[child] == 1) {
                    bool allRespect = true;//遍历的是待删除节点的孩子节点的孩子节点 
                    for (int grandchild : children[child]) {//孩子的子节点都不尊重它,我在想,这个是怎么来的? 
                        if (respect[grandchild] == 0) {//为什么在打了标记以后,还会产生新的子节点,这个字节点的子节点都不尊重它 
                            allRespect = false;//
                            break;
                        }
                    }
                    if (allRespect && !canDelete[child]) {
                        toDelete.push(child);0
                        canDelete[child] = true;
                    }
                }
            }
        }

        // Check if parent can now be deleted
        if (par != -1 && respect[par] == 1 && !canDelete[par]) {
            bool allRespect = true;
            for (int sibling : children[par]) {
                if (sibling != node && respect[sibling] == 0) {
                    allRespect = false;
                    break;
                }
            }
            if (allRespect) {
                toDelete.push(par);
                canDelete[par] = true;
            }
        }
    }

    // Output results
    if (result.empty()) {
        cout << -1 << endl;
    } else {
        for (int id : result) {
            cout << id << " ";
        }
        cout << endl;
    }

    return 0;
}

(这是从网上找的代码)

看完这篇代码,我陷入沉思,我是觉得,如果第一次遍历整个图,给那些要删除的节点打标记的时候,如果一个节点没有被打上标记,令它不尊重它的父母为q,它的所有孩子顶点也不尊重它为p,则情况有p1q0,p0q1,p0q0,那么有可能在删除了子节点以后改变它的待删除状态的情况只有p1q0,那也就是说,节点的删除可以改变q的真假,如果原来q为0了,那也就是说,至少存在一个尊重父节点的子节点,而节点的删除过程中,不可能删除q为0的节点,也就是说,pq的真假性是无法被改变的。

通过上面简短的证明,我发现了一个问题,好像只有第一次遍历是有用的,后面那个BFS就是nmd花架子,是拿来看的,但是我觉得,这可是题解啊,题解不会多写一条无用代码。于是我一直在想是那里错了,我想找一个反例,但是一直找不出来,于是浪费了4个小时想这个东西,期间找了很多人来探讨这个问题。我也在想这个证明是不是错的

直到我在codeforces上面看到了这一个题解,其思路是找到不用删的节点,如果有子节点是0或者是根节点,那就不需要删除,然后给它去掉标记;最后遍历一下标记了的节点就好了。。。就是这么简单易懂

天哪,我去,这么短,这就是图论的魅力嘛,我心想。结果仔细看,这就是我证明过的那个想法。只是取了个反而已。我之前的想法没有错!然后我又想,为什么大佬要用这个反着来的方法呢?因为这样子在输入的时候就可以确定删除情况了。

于是我马上仿制出了我的代码

#include<iostream>
#include<queue>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;//假设删除没有后效性
static const int BN=100001;

int main() {
	int n;
	cin>>n;
	vector<int> respect(n+1,1);
	for(int i=1;i<=n;i++){
		int t1,t2; cin>>t1>>t2;
		if(t1==-1){
			respect[i]=0;
			continue;
		}
		if(t2==0){
			respect[i]=0;
			respect[t1]=0;//只要这个节点是0,那么就不用删除,父节点不用删除
		}
	}//存储图的信息
	int ct=0;
	for(int i=1;i<=n;i++){
		if(respect[i]==1){
			cout<<i<<' ';
			ct++;
		}
	}
	if(ct==0){
		cout<<-1;
	}
	cout<<endl;
    return 0;
}



运行了,是对的。

在那之后,我把我在网上找到的题解的BFS给删了,重新跑了一遍,代码如下

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> parent(n, -1);
    vector<int> respect(n);
    vector<vector<int>> children(n);
    vector<bool> canDelete(n, false);
    queue<int> toDelete;
    vector<int> result;

    // Read input
    int root = -1;
    for (int i = 0; i < n; ++i) {
        int p, c;
        cin >> p >> c;
        if (p == -1) {
            root = i;
        } else {
            p--; // Convert 1-based to 0-based
            parent[i] = p;
            children[p].push_back(i);
        }
        respect[i] = c;
    }

    // Determine initial deletable nodes
    for (int i = 0; i < n; ++i) {
        if (i != root && respect[i] == 1) {
            bool allRespect = true;
            for (int child : children[i]) {
                if (respect[child] == 0) {
                    allRespect = false;
                    break;
                }
            }
            if (allRespect) {
                toDelete.push(i);
                canDelete[i] = true;
            }
        }
    }

    // Process deletable nodes
    while (!toDelete.empty()) {
        int node = toDelete.front(); toDelete.pop();
        result.push_back(node + 1);
    }

    // Output results
    if (result.empty()) {
        cout << -1 << endl;
    } else {
        for (int id : result) {
            cout << id << " ";
        }
        cout << endl;
    }

    return 0;
}

你们看,是不是这样啊。

(其实那个res队列也没啥用,只是我不想改了)

然后结果,竟然是对的。。。。。。。。。。。。。。。

艹艹艹艹艹艹这什么题解啊,害我浪费了半天时间

这个经历告诉我,有的时候证明一些结论可以节约很多模拟的时间,但是如果拿不准的话就会身陷囫囵,一下子想到固然牛逼,但是在比赛上还是老老实实写模拟吧,这样至少速度快能拿部分分,之后再来优化代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值