【问题描述】
给定一颗有根树,顶点编号为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 , 则v 的所有子顶点与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队列也没啥用,只是我不想改了)
然后结果,竟然是对的。。。。。。。。。。。。。。。
艹艹艹艹艹艹这什么题解啊,害我浪费了半天时间
这个经历告诉我,有的时候证明一些结论可以节约很多模拟的时间,但是如果拿不准的话就会身陷囫囵,一下子想到固然牛逼,但是在比赛上还是老老实实写模拟吧,这样至少速度快能拿部分分,之后再来优化代码。