Codeforces Round #743 (Div. 2) C. Book(拓扑排序,dp,优先队列)

题意

一本书有n个章节,每个章节都有可能有前置章节,对于某个章节i只有理解完他的全部前置章节才能理解章节i,每次看书从第一章看到最后一章,求最少要多少次能理解全部章节,没办法理解所有章节则输出-1。
思路
看题意就能比较容易看出来是有向图的拓扑排序,对于章节i由di个前置章节那么他的入度就是di,理解了章节i的其中一个前置章节那么di–,只有当di=0的时候才能理解章节i。如果有向图存在环,那么就无解

以下给出两种解法:
1.dp
对于某个章节x,如果他的前置章节y的编号小于x,那么章节x和y可能同时理解,因为每一遍读书从第一张读到最后一章,所以y编号小于x的时候可能在同一遍读书的时候理解,那么dp[x]=max(dp[x],dp[y]),如果y的编号大于x,那么x至少需要在y的下一遍看书的时候才能理解他也就是dp[x]=max(dp[x],dp[y]+1).这样子可以给出状态转移方程:
x>y时,dp[x]=max(dp[x],dp[y])
x<y时,dp[x]=max(dp[x],dp[y]+1)
有了这个状态转移方程我们只需要在拓扑排序将入读为0的点拿出来的时候,更新以这个点为前置节点的dp值就好了,入度为0的点可以不用优先队列存储,统计答案的时候只需要取最大的dp值就可以了。
这里可能会有小疑问在一个节点有多个前置节点的情况下能否取得正确答案,一个节点在哪一次读完只和它的前置节点读完的时间有关,而且我们在更新的时候是将入读为0的节点拿出来更新以它为前置节点的节点,那么保证拿出来的这个节点以后都不会被更新了,所以可以放心大胆的用它去更新以他为前置节点的节点。
代码:

#include<bits/stdc++.h>
using namespace std;
int T,n,tot,ans,num;
//ans存储答案,num用于判断有向图中是否有环
const int maxn=1e6+10;
int d[maxn],head[maxn],v[maxn],Next[maxn],dp[maxn];
//d[i]为i节点的入度,head[i]存储i节点指向的第一条边的编号,v[i]表示第i条边的终点
//Next[i]表示第i条边的下一条边,dp数组如题解中的dp数组
queue<int>q;//存储入度为0的点
void init()//初始化
{
	num=n;
	tot=0;
	ans=1;
	for(int i=1;i<=n;i++)
	head[i]=d[i]=0,dp[i]=1;
}

void add(int x,int y)//无向图加边
{
	v[++tot]=y;Next[tot]=head[x];head[x]=tot;
}
void bfs()
{
	while(q.size())
	{
		int x=q.front();
		q.pop();
		num--;
		ans=max(ans,dp[x]);
		for(int i=head[x];i;i=Next[i])
		{
			//节点x入度为0,更新以x为前置节点的所有节点
			int y=v[i];
			d[y]--;
			if(y<x)//状态转移
			dp[y]=max(dp[y],dp[x]+1);
			else
			dp[y]=max(dp[y],dp[x]);
			if(!d[y])
			{
				q.push(y);
			}
		}
	}
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		init();
		for(int i=1;i<=n;i++)
		{
			int len;
			scanf("%d",&len);
			for(int j=1;j<=len;j++)
			{
				int t;
				scanf("%d",&t);
				add(t,i);//添加边
				d[i]++;
			}
		}
		for(int i=1;i<=n;i++)//拓扑排序初始化
		{
			if(!d[i])
			q.push(i);
		}
		bfs();//拓扑排序

		if(num)
		printf("-1\n");
		else
		printf("%d\n",ans);
	}

return 0;
}

2.双重优先队列
这个方法是在模拟读书的过程,开两个优先队列q1,q2,q1存储当前哪些书能够在这一趟读完,q2存储那些书能够在下一趟读完,只能在下一次读完的条件是当前这个点从q1中取出来的时候,比这一趟读书编号最大的节点编号小,那么把他加入q2。在一遍读书中q1为空之后如果q2不为空说明还需要读一遍,交换q1和q2,这样讲可能不太好理解,看一下下面的代码和注释。

#include<bits/stdc++.h>
using namespace std;
int T;
int d[200005];//存储入度
int head[200005],net[2000005],v[2000000];
//前向星存图
int tot=0;//边的数量
int n;
int num;//判断优先图中是否有环
int rans=0;//答案数
priority_queue<int,deque<int>,greater<int> >q;
priority_queue<int,deque<int>,greater<int> >q2;

void add(int x,int y)
{
    tot++,v[tot]=y,net[tot]=head[x],head[x]=tot;
}

void init()//初始化
{
    rans=0;
    num=n;
    tot=0;
    while(q2.size())
    q2.pop();
    for(int i=1;i<=n;i++)
    {
        d[i]=head[i]=0;
    }
}

void bfs()
{
    while(q.size())
    {
        rans++;//需要读的次数+1
        int lst=0;//这一遍读书当前读过的最大的章节编号
        while(q.size())
        {
            int x=q.top();
            q.pop();
            if(lst>x)//如果x的章节小于当前读过的最大章节编号,那么放在下一遍读
            {
                q2.push(x);//q2存放当前这一遍没办法读完的章节
            }else{//如果大于度过的最大章节编号
                lst=x;//更新最大章节编号
                for(int i=head[x];i;i=net[i])//拓扑排序
                {
                    int y=v[i];
                    d[y]--;
                    if(!d[y])
                    q.push(y),num--;

                }
            }
        }
        if(q2.size())//如果下一遍还有书没读完,那么交换q和q2
        {
            swap(q,q2);
        }
    }
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d",&n);
        init();
        for(int i=1;i<=n;i++)//读入有向图
        {
            int t;
            scanf("%d",&t);
            for(int j=1;j<=t;j++)
            {
                int t1;
                scanf("%d",&t1);
                add(t1,i);
                d[i]++;
            }
        }
        
        for(int i=1;i<=n;i++)//将入度为0的节点加入队列
        {
            if(d[i]==0)
            q.push(i),num--;
        }

        bfs();//拓扑排序
        if(num)//图中存在环
        {
            printf("-1\n");
            continue;
        }
        
        printf("%d\n",rans);//输出答案
    }
    
return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值