洛谷 P2763 试题库问题

题目描述

«问题描述:

假设一个试题库中有n道试题。每道试题都标明了所属类别。同一道题可能有多个类别属性。现要从题库中抽取m 道题组成试卷。并要求试卷包含指定类型的试题。试设计一个满足要求的组卷算法。

«编程任务:

对于给定的组卷要求,计算满足要求的组卷方案。

输入输出格式

输入格式:

 

第1行有2个正整数k和n (2 <=k<= 20, k<=n<= 1000)

k 表示题库中试题类型总数,n 表示题库中试题总数。第2 行有k 个正整数,第i 个正整数表示要选出的类型i的题数。这k个数相加就是要选出的总题数m。接下来的n行给出了题库中每个试题的类型信息。每行的第1 个正整数p表明该题可以属于p类,接着的p个数是该题所属的类型号。

 

输出格式:

 

第i 行输出 “i:”后接类型i的题号。如果有多个满足要求的方案,只要输出1个方案。如果问题无解,则输出“No Solution!”。

 

输入输出样例

输入样例#1:

3 15
3 3 4
2 1 2
1 3
1 3
1 3
1 3
3 1 2 3
2 2 3
2 1 3
1 2
1 2
2 1 2
2 1 3
2 1 2
1 1
3 1 2 3

输出样例#1:

1: 1 6 8
2: 7 9 10
3: 2 3 4 5

说明

感谢 @PhoenixEclipse 提供spj

 

 

网络流

               网络流是图论中的一种理论与方法,研究网络上的一类最优化问题 。1955年 ,T.E.哈里斯在研究铁路最大通量时首先提出在一个给定的网络上寻求两点间最大运输量的问题。1956年,L.R. 福特和 D.R. 富尔克森等人给出了解决这类问题的算法,从而建立了网络流理论。所谓网络或容量网络指的是一个连通的赋权有向图 D= (V、E、C) , 其中V 是该图的顶点集,E是有向边(即弧)集,C是弧上的容量。此外顶点集中包括一个起点和一个终点。网络上的流就是由起点流向终点的可行流,这是定义在网络上的非负函数,它一方面受到容量的限制,另一方面除去起点和终点以外,在所有中途点要求保持流入量和流出量是平衡的。如果把下图看作一个公路网,顶点v1…v6表示6座城镇,每条边上的权数表示两城镇间的公路长度。现在要问 :若从起点v1将物资运送到终点v6去 ,应选择那条路线才能使总运输距离最短?这样一类问题称为最短路问题 。 如果把上图看作一个输油管道网 , v1 表示发送点,v6表示接收点,其他点表示中转站 ,各边的权数表示该段管道的最大输送量。现在要问怎样安排输油线路才能使从v1到v6的总运输量为最大。这样的问题称为最大流问题。  ————《百度百科》

 

没有接触过网络流的朋友可以去看下这篇博客:

https://www.cnblogs.com/SYCstudio/p/7260613.html

 

我们进入正题!

“网络流是我造诣最深的几个算法之一,在我的巅峰时期,只要是网络流的题给我看几分钟我就能秒。而秒掉网络流的关键所在就是建图。”

                                                                                                                                                                                          ————PB

 

现在我们有N道题,K个类别。而同一道题有Pi个类别属性。我们被要求选出M道题并符合条件(每个类型选Ti道题)。

显然这是一道类似二分图匹配的题,每个题目对应一些类别,而我们要求最大的一个能满足M个匹配的方案。

所以直接简图,跑一遍最大流,如果最大流等于M,输出方案。

 

接下来是建图的问题,理所当然的,每道题只能选一次,所以从超级原点S,连一条1的边到每道题。

每道题只能并入一个类型中,所以从每道题连Pi条1的边到这道题所对应的类型。

因为第i个类型的题要选Ti道,所以从每个类型的点连一条为Ti 的边到超级汇点T(T=K+N+1)。

(没有标出的边权值都为1,这个图和样例无关)

建完图后,直接跑网络流就好了。判断最大流是否等于题目数。

剩下的是要输出路径。

通常来说,网络流输出路径只需要找到残量为0的边跑DFS,设个bool数组判断是否走过就好了。

由于有SPJ,所以也没有什么限制,路径可以跑的很随意。  下面放出代码,代码中不会有太多的注释。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=2001;
const int INF=104859600;
struct edge//存图
{
	int node,w,next;
}h[MAXN*10];
int Head[MAXN],Depth[MAXN],Stack[MAXN];//Head表示邻接表的头指针,Depth表示分层图深度。
int k,n,m,p,tot=-1,x,y,kind,sum=0,s,t,top,ans=0;
bool visit[MAXN];
inline void add(int u,int v,int w)//加边
{
	h[++tot].next=Head[u];
	h[tot].node=v;
	h[tot].w=w;
	Head[u]=tot;
}
inline bool bfs()//跑BFS将图分层
{
	int q[MAXN*10],head=1,tail=1;
	memset(Depth,0,sizeof(Depth));
	Depth[s]=1;
	q[head]=s;
	while(head<=tail)
	{
		int x=q[head++];
		for(register int i=Head[x];i!=-1;i=h[i].next)
		{
			int v=h[i].node;
			if(h[i].w>0&&Depth[v]==0)
			{
				Depth[v]=Depth[x]+1;
				q[++tail]=v;
			}
		}
	}
	return Depth[t];
}
inline int dfs(int x,int dist)//跑DFS获取最大流
{ 
	if(x==t)
	   return dist;
	for(register int i=Head[x];i!=-1;i=h[i].next)
	{
		int v=h[i].node;
		if(h[i].w>0&&Depth[v]==Depth[x]+1)
		{
			int di=dfs(v,min(dist,h[i].w));
			if(di>0)
			{	
				h[i].w-=di;
				h[i^1].w+=di;
				return di;
			}
		}
	}
	return 0;
}
inline void run(int x)//输出路径
{
	for(register int i=Head[x];i!=-1;i=h[i].next)
	{
		int v=h[i].node;
		if(h[i].w==0&&visit[v]==0)
		{
			Stack[++top]=v-k;
			visit[v]=1;
		}
	}
}
int main()
{
	memset(Head,-1,sizeof(Head));
	scanf("%d %d",&k,&n);
	s=0,t=n+k+1;
	for(register int i=1;i<=k;i++)
	{							
		scanf("%d",&kind);		 
		sum+=kind;
		add(s,i,kind);	
		add(i,s,0); 	//这里和上面讲的有点不一样。
	}		//为了方便调试(样例类型少)我把原点和类型连接把题目和汇点连接
			//实际上是没有区别的
	for(register int i=1;i<=n;i++)
	{
		add(i+k,t,1);
		add(t,i+k,0);
		scanf("%d",&p);
		for(register int j=1;j<=p;j++)
		{
			scanf("%d",&y);
			add(y,i+k,1);
			add(i+k,y,0);
		}	
	}
	while(bfs())
		ans+=dfs(s,INF);
	if(ans!=sum)
		puts("No Solution!");
	else
	{
		for(register int i=1;i<=k;i++)
		{	
			top=0;
			run(i);
			printf("%d: ",i);
			for(int j=top;j>=1;j--)
				printf("%d ",Stack[j]);
			puts("");
		}
	}	
	return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值