[题目要求]
http://n.boj.me/onlinejudge/newoj/showProblem/show_problem.php?problem_id=14
http://poj.org/problem?id=3687
[题目涉及的相关理论与算法]
1,图的拓扑排序以及演变。
2,贪心算法或者采用优先队列。
[题目中需要注意的地方]
1,标号问题,一定要仔细阅读题目,最后输出的是“按照标号顺序的重量列表”,我们拓扑排序的结果是“按照重量顺序的标号列表”。要转换一下。
2,可能会重复输入一个次序。。。我们必须在每次读入一个先后次序的时候,检测是否已经读入过,因为重复累计节点的出度会导致结果混乱。这个很容易忽略。。
3,注意输出结果的顺序不是字典序(他们都是拓扑出来的合理结果),字典序是“相同位置优先”,这里则是从标号最小的开始,越靠前越优先,比如拓扑的结果(注意不是最后的序列):4132比3241更符合题目要求,因为1更靠前,但是从字典序的角度却相反。
[思路过程]
这个题花了挺长时间的。。但是并不沮丧。。看到网上有的两个小时就解决了,毕竟人家是做很多的。。我才刚刚起步嘛,很多东西都是遇到就学的。。所以学习新东西占了时间是最多的,调试什么的说白了也就半天就差不多了,最一开始还不知道拓扑排序,想成简单的插入排序了。。把给定的先后顺序直接相互插在新的链表里,写了半天总是WA,然后恍然大悟想到 比如 1-3,4-3,可以简单的插成1-4-3,但是如果后面再来一个4-1,问题就来了。。就会认为是出现环了,而显然4-1-3就是正确的,另外,它即使排成了也是字典序(那时还没意识到字典序和题目中的顺序的区别。。)
后来查了一下,知道了拓扑排序,然后顺其自然写了一个,正向拓扑排序,但是拓扑排序的结果是很多的,按照对题目的理解,正向采用贪心,结果还是WA,网上查了查,看到了同样的问题。。哦,这时意识到了这时字典序。。不是题目中的要求,然后看到了可以使用反向字典序(就是考虑出度而不是入度),得出的结果就是题目要求的拓扑序。
本以为结果很显然了,但是还是WA,快疯了。。。想到查一查PKU的discuss,哎。。怎么才想到要看呢。。,看到了一组神奇的数据,才终于明白前面说的第一个注意的地方。。
6 8
1 2
1 3
1 4
3 2
3 5
4 5
6 4
6 5
输出的结果是:
1 3 2 6 4 5
还是
1 3 2 5 6 4
呢?
我开始想题目中有4 5,显然第二个是不对的。。结果找了一个AC的程序才发现答案是第二个。。然后终于顿悟。。然后升华=。=
好了,后面就不多说了,上代码了。。我没用拓扑排序中的入栈出栈,而是逆向贪心。。因为觉得这个顺其自然就理解了哈哈,存储形式是邻接表,邻接表中记录边用的是list容器,,刚学的容器……正好习惯一下~
[代码]
#include<iostream>
#include<list>
#include<cstring> //这个包含memset函数。
#include<algorithm> //后面有判重边时需要查找list中是否含有此边,用到了find方法。
using namespace std;
int const MAX = 201;
int const NONE = -1; //拓扑排序中“删除或者访问过的节点标记”
struct itemNode{
int outdegree;
list<int> adNode;
}; //由于是逆向建图,所以讨论用的是出度,这个当然都是相对的
void caseSolve(int m) //子函数,打印出要求的序列
{
itemNode NodeList[MAX];
int sort[MAX]; //这个记录的是最后的结果
int result[MAX]; //这个记录的是按照拓扑之后的序号。。。最后就是这里差点出现问题
memset(result,0,sizeof(result));
int numM=1,step=0; //numM用来记录拓扑结果的序列。step是一个变量用来比对以判断
//是否存在环(也就是说有环之后必然有节点访问不到就跳出了)
for(int i=1;i<m+1;i++) //初始化,每个出度设为0
{
NodeList[i].outdegree=0;
}
int numCase;
cin>>numCase;
int numL,numR; //numCase记录边的个数(包含重边),numL,numR就是输入的前后顺序啦
while((numCase--) != 0)
{
cin>>numL>>numR;
if(find(NodeList[numR].adNode.begin(),NodeList[numR].adNode.end(),numL) == NodeList[numR].adNode.end())
{ //if里面是看是否出现了重边,也就是说numR节点的list中记录的是指向此点的来源标号。如果
//如果来源里已经有了numL,那么就是重边了,就不进行下面的统计了
NodeList[numL].outdegree ++; //不是重边,那么出度加1
NodeList[numR].adNode.push_back(numL);//然后后面节点的list记录“是哪些
//点到过我这里来呢”
}
}
for(int i=0;i<m;i++) //拓扑排序过程+贪心过程,外层是因为共有m个节点,
//所以按照道理是每次找到一个点,所以共m次。
{
for(int j=m;j>=1;j--)//反向拓扑,倒过来看谁大(这是贪心的过程),而不是正向找最小。
//这是这道题比较贱的地方。
{
if(NodeList[j].outdegree == NONE) continue;//访问过了。。那就跳过。
if(NodeList[j].outdegree == 0)
{
result[numM]=j;
numM++;
step++; //找到没有出度的点,而且是倒过来的,所以
//他就是目前最大的符合题目要求的最“后”的点,进入数组。
NodeList[j].outdegree = NONE;//找到就标记已访问
while(! NodeList[j].adNode.empty())
{ //这个循环就是把所有和删除的点有关系的点遍历,然后把它们的出度减一。
//拓扑排序的核心。
int temp = NodeList[j].adNode.front();
NodeList[j].adNode.pop_front();
NodeList[temp].outdegree--;
}
break;//一旦找到这一轮,我们就推出去从头再来,而不是继续,
//因为我们是在“贪心”!这里也可以用优先队列方式处理,注意是优先队列,而不是FIFO队列。。
}
}
}
if(step !=m ) cout<<"-1";//好了,如果点数没够,说明,按照一个环自己就出来了,,
//而冷落了别的点,所以说明不存在拓扑序。
else
{
for(int i=m;i>=1;i--)
{
int temp = m-i+1;
sort[result[i]]=temp; //最后输出的是“按照标号顺序的重量列表”,我们
//拓扑排序的结果是“按照重量顺序的标号列表”。要转换一下。
}
cout<<sort[1];
for(int i=2;i<=m;i++)
{
cout<<" "<<sort[i];
}
}
cout<<endl;
}
int main() //以下是主程序,没有什么可说的了~
{
//freopen("in.txt", "r", stdin);
//freopen("out.txt", "w", stdout);
int t;
cin>>t;
while((t--) > 0)
{
int m;
cin>>m;
caseSolve(m);
}
//fclose(stdin);
//fclose(stdout);
return 0;
}
[一点小积累]
1,memset用法。
2,STL中的find方法。
3,拓扑排序可以查找图中是否含有环。
[尾声]
时间越长就越痛苦。。而且一想到问题可能还是很幼稚的时候就更沮丧。。
不过AC的时候还是很高兴的。。最后推荐这首背景歌曲,今天豆瓣的时候淘到的,真安静~