Libre 6003 「网络流 24 题」魔术球 (网络流,最大流)

Libre 6003 「网络流 24 题」魔术球 (网络流,最大流)

Description

假设有n根柱子,现要按下述规则在这n根柱子中依次放入编号为 1,2,3,4......的球。
(1)每次只能在某根柱子的最上面放球。
(2)在同一根柱子中,任何2个相邻球的编号之和为完全平方数。
试设计一个算法,计算出在n根柱子上最多能放多少个球。例如,在4 根柱子上最多可 放11个球。
´编程任务:
对于给定的n,计算在 n根柱子上最多能放多少个球。

Input

第1 行有 1个正整数n,表示柱子数。

Output

第一行是球数。接下来的 n行,每行是一根柱子上的球的编号。

Sample Input

4

Sample Output

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

Http

Libre:https://loj.ac/problem/6003

Source

网络流

解决思路

这道题不好想到是网络流的题目。
博主觉得自己的解释有些问题,等博主想清楚了再写吧。。。(2018.1.2 Update)
我们从小到大依次枚举最大能放入的球的大小。对于每一个球,我们将其拆成两个,一个入点一个出点。从源点向入点连一条流量为1的边,从出点向汇点也连一条流量为1的边,这样是为了保证一个球要么上面或下面没有球要么每一面至多有一个球。当我们在枚举球x时,从1到x-1循环看一看有那些球满足它与x的和是完全平方数。如果是,则在那个球的出点与x的入点之间连一条流量为1的边,同样是为了保证只有一个球。
通过上面这样的模型转换,我们就把题目变成了求最少路径覆盖。关于最少路径覆盖,可以到我的这一篇文章查看
然后我们就可以每一次加入一个数即其相应的边,跑一边最大流求出最小路径覆盖,如果当前的路径覆盖数大于我们输入的柱子数,则说明答案是当前-1.
需要注意的是因为我们是逐渐加边的,即每一次其实是在上一次的残量网络上加边,所以统计路径覆盖数时不要重新统计,而是在原来的基础上进行统计。
至于如何输出一组方案呢?我们把图全部重新建一遍,再跑最大流,然后从小到大扫描每一个数,若发现该数还未访问,则从该数出发,寻找它的流流向了哪个点,循环输出,直到某个点的出点是到汇点,把这条路径上的点全部打上标记。这样对比输出即可。

另:这里使用Dinic实现最大流,关于Dinic算法,请移步我的这篇文章

代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

const int maxN=5000;
const int maxM=100001;
const int inf=2147483647;

class Edge
{
public:
	int u,v,flow;
};

int n,m;
int cnt=-1;
int Head[maxN];
int Next[maxM];
Edge E[maxM];
int depth[maxN];
int cur[maxN];
int Q[maxM];
bool vis[maxN];

void Add_Edge(int u,int v,int flow);
bool bfs();
int dfs(int u,int flow);

int main()//笔者这里用0代表源点,1代表汇点;而对于每一个点u,其拆点后的入点为u*2,出点为u*2+1
{
	int sum;
	memset(Head,-1,sizeof(Head));
	scanf("%d",&sum);
	int Ans=0;
	n=0;
	do//从1开始加点和加边
	{
		n++;
		Ans++;//注意这里,每一次开始时,加入一个数相当于要多一条路径,因为我们还没有把其放入任何以前的路径中
		Add_Edge(0,n*2,1);//先连上n与汇点和n与源点
		Add_Edge(n*2+1,1,1);
		for (int i=1;i<n;i++)//循环扫描以前的数,若满足和为完全平方数,则连边
			if (sqrt(n+i)==(int)(sqrt(n+i)))
				Add_Edge(i*2,n*2+1,1);
		while (bfs())
		{
			for (int i=0;i<=n*2+1;i++)
				cur[i]=Head[i];
			while (int di=dfs(0,inf))
				Ans-=di;//注意这Ans是减,因为当增广出一条新的路径时,说明原路径数减1
		}
		if (Ans>sum)//若发现此时路径数已经比柱子数大了,则说明前一个是答案,退出并减一
		{
			n--;
			break;
		}
	}
	while (1);
	cout<<n<<endl;
	for (int i=0;i<=cnt;i++)//重新建图
		if ((E[i].u<E[i].v)||(E[i].v==1))
			E[i].flow=1;
		else
			E[i].flow=0;
	Ans=0;//再跑一边最大流
	while (bfs())
	{
		for (int i=0;i<=n*2+1;i++)
			cur[i]=Head[i];
		while (int di=dfs(0,inf))
			Ans+=di;
	}
	memset(vis,0,sizeof(vis));//输出方案
	for (int i=1;i<=n;i++)
	{
		if (vis[i]==1)
			continue;
		int now=i;
		bool get=0;
		do
		{
			cout<<now<<' ';
			vis[now]=1;
			get=0;
			for (int i=Head[now*2];i!=-1;i=Next[i])
				if ((E[i].v%2!=0)&&(E[i].flow==0))
				{
					get=1;
					now=E[i].v/2;
					break;
				}
		}
		while (get==1);
		cout<<endl;
	}
	return 0;
}

void Add_Edge(int u,int v,int flow)
{
	cnt++;
	Next[cnt]=Head[u];
	Head[u]=cnt;
	E[cnt].u=u;
	E[cnt].v=v;
	E[cnt].flow=flow;

	cnt++;
	Next[cnt]=Head[v];
	Head[v]=cnt;
	E[cnt].u=v;
	E[cnt].v=u;
	E[cnt].flow=0;
}

bool bfs()
{
	memset(depth,-1,sizeof(depth));
	int h=1,t=0;
	Q[1]=0;
	depth[0]=1;
	do
	{
		t++;
		int u=Q[t];
		for (int i=Head[u];i!=-1;i=Next[i])
		{
			int v=E[i].v;
			if ((depth[v]==-1)&&(E[i].flow>0))
			{
				depth[v]=depth[u]+1;
				h++;
				Q[h]=v;
			}
		}
	}
	while (h!=t);
	if (depth[1]==-1)
		return 0;
	return 1;
}

int dfs(int u,int flow)
{
	if (u==1)
		return flow;
	for (int &i=cur[u];i!=-1;i=Next[i])
	{
		int v=E[i].v;
		if ((depth[v]==depth[u]+1)&&(E[i].flow>0))
		{
			int di=dfs(v,min(flow,E[i].flow));
			if (di>0)
			{
				E[i].flow-=di;
				E[i^1].flow+=di;
				return di;
			}
		}
	}
	return 0;
}
原文地址:https://www.cnblogs.com/SYCstudio/p/7280403.html