P4376题解
核心算法&前置知识:拓扑排序+二分
此题解默认大家已经掌握关于以上算法的基础知识。
同时也默认大家会链式前向星建图点进这题的大佬肯定都会。
Part1:看题+手玩样例
遇到这种具有多种有关优先级条件的题,我们一般首先考虑按照他们的优先顺序进行建图,然后进行拓扑排序。
以本题样例为例,通过FJ的第一次观察结果可知,为奶牛们挤奶的优先级是
那么结合FJ第二次观察的结果,优先级是
如果你用此图来跑拓扑排序的话,那么拓扑序会有两个,分别是
那我们继续来结合FJ第三次观察的结果,此时我们的图变成了这样:
这时候,我们的图出现了环,意味着有几个条件是互相矛盾的,所以此时这张图并没有拓扑序。
既然有环的时候无解,那我们还得判断这个图是否带环,那我们到底该怎么判断呢?
Part2:拓扑排序黑科技:判环
我们以下面的这张有向有环图为例。
我们来按照拓扑排序的流程走一遍:此时我们先删掉入度为
此时所有的点入度都不为
我们知道,当这张图上所有的点都被删去时,拓扑排序才能进行下去,这也侧面说明这张图是一张有向无环图。如果拓扑排序进行不下去时,图上还有点没有被删,那么这张图一定是一张有环的图。
因此,我们只需要在拓扑排序结束之后,判断所有点的入度是否全部为
代码:
queue<int>q;
for(int i=1;i<=n;i++)
if(!in[i])
q.push(i);
while(!q.empty())
{
int now=q.front();
q.pop();
for(int i=head[now];i;i=a[i].nxt)
{
int to=a[i].v;
in[to]--;
if(!in[to])q.push(to);
}
}
int tot=0;
for(int i=1;i<=n;i++)
if(!in[i])tot++;
if(tot<n)return false;
return true;
那这个时候我们就有了一个非常暴力的思路,既然FJ想要使题目中的
但这样做时间复杂度肯定是爆炸的,这是一个时间复杂度为平方级别的做法。
那我们该怎么优化呢?
Part3:二分
一个显然的结论,如果一张图上有环,那么它无论加上任意数量条边,这张图依然会有环。如果一张图上无环,那么它无论删去任意数量条边,这张图必定没有环。
这不是废话吗。
所以我们发现,定义
这便说明,求
那这样我们就实现了这份暴力代码的优化。
Part4:完整代码
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
const int MAXN=1e5+5;
int n,m,cnt,head[MAXN],in[MAXN];
vector<int>v[MAXN];
struct edge
{
int v,nxt;
}a[MAXN*2];
void addedge(int u,int v)
{
a[++cnt].v=v;
a[cnt].nxt=head[u];
head[u]=cnt;
}
void build(int x)
{
cnt=0;
memset(head,0,sizeof(head));
memset(in,0,sizeof(in));
for(int i=1;i<=x;i++)
for(int j=0;j<v[i].size()-1;j++)
{
addedge(v[i][j],v[i][j+1]);
in[v[i][j+1]]++;
}
}
int check(int x)
{
build(x);
queue<int>q;
for(int i=1;i<=n;i++)
if(!in[i])
q.push(i);
while(!q.empty())
{
int now=q.front();
q.pop();
for(int i=head[now];i;i=a[i].nxt)
{
int to=a[i].v;
in[to]--;
if(!in[to])q.push(to);
}
}
int tot=0;
for(int i=1;i<=n;i++)
if(!in[i])tot++;
if(tot<n)return false;
return true;
}
void get_ans(int x)
{
build(x);
priority_queue<int,vector<int>,greater<int> >q;//使用优先队列 保证这个拓扑序的字典序最小
for(int i=1;i<=n;i++)
if(!in[i])
q.push(i);
while(!q.empty())
{
int now=q.top();
q.pop();
cout<<now<<" ";
for(int i=head[now];i;i=a[i].nxt)
{
int to=a[i].v;
in[to]--;
if(!in[to])q.push(to);
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int si;
cin>>si;
for(int j=1;j<=si;j++)
{
int x;
cin>>x;
v[i].push_back(x);
}
}
int l=1,r=m;
while(l<=r)
{
int mid=(l+r)/2;
if(check(mid))l=mid+1;
else r=mid-1;
}
int ans;
for(ans=r;ans<=l;ans++)if(check(ans))break;
get_ans(ans);
return 0;
}
Part5:P.S.
快速将图删除的方法:将
二分时不知道
int ans;
for(ans=r;ans<=l;ans++)if(check(ans))break;
这样即可求出本次二分的正确答案。感觉有点暴力实际上跑的还是很快的,因为二分过后