例2:NC213818 [网络流24题]魔术球问题

题目:

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

题解:

首先思想很重要,不是明显的图论,要往图论方向靠
在这里插入图片描述

建图方式:
S与左图所有数相连,右图所有数与T相连

在这里插入图片描述
对于一个进来的编号的球x,有两个选择:
第一个选择是放在某个和他能组成平方数的球(我们起名为y)的后面
第二个选择是自己单独出来成为开头
对于第一种选择,当然要y和x连一个边
对于第二种选择,S要和x连一个边
当然我们也要让x与T连一个边
但是一个点怎么能同时连多个,显然一个点是不够用的
我们将一个点给分开,分为x和x'
x与s相连,x'与t相连
为了满足第一种情况,对于满足条件的x和y两个点,连接y和x'
这样组成的图直接跑最大流算法即可
球和柱子同时加,跑出的最大流为省掉的柱子数,如果加入一个数当前最大流没有变化,说明柱子数+1,如果最大流+1,说明可以省下一个柱子、
在这里插入图片描述

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<iomanip>
#include<algorithm>
#include<cmath>
#include<queue>
#define in(x) scanf("%d",&x)
using namespace std;
const int maxn=1e5;
int n,all=0,num=0,nxt[maxn],to[maxn],head[maxn],d[maxn];
int w[maxn],cnt=1,s=0,t=50003,re[maxn],xia[maxn],vis[maxn];
queue<int>q;
void add(int x,int y,int we)
{
    nxt[++cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=we;
    nxt[++cnt]=head[y];head[y]=cnt;to[cnt]=x;w[cnt]=0;
}
int bfs()
{
    while(q.size()) q.pop();q.push(s);
    memset(d,0,sizeof(d));d[s]=1;
    while(q.size())
    {
        int x=q.front();q.pop();
        for(int i=head[x];i;i=nxt[i])
        {
            int u=to[i];
            if(d[u]||w[i]<=0) continue;
            d[u]=d[x]+1;
            q.push(u);
        }
    }
    return d[t];
}
int dfs(int x,int flow)
{
    if(x==t) return flow;
    int k;
    for(int i=head[x];i;i=nxt[i])
    {
        int u=to[i];
        if(d[u]!=d[x]+1||w[i]<=0) continue;
        if(k=dfs(u,min(w[i],flow)))
        {
            w[i]-=k;
            w[i^1]+=k;
            if(u!=t) xia[x>>1]=u>>1;
            return k;
        }
    }
    return 0;
}
int dinic()
{
    int k=0;
    while(bfs())
    {
        while(1)
        {
            int p=dfs(s,1e9);
            if(!p) break;
            k+=p;
        }
    }
    return k;
}
int main()
{
    in(n);
    while(all<=n)
    {
        num++;
        add(s,num<<1,1);
        add((num<<1)|1,t,1);

        for(int i=sqrt(num)+1;i*i<2*num;++i)//能组成平方数的数建边 
        add((i*i-num)<<1,(num<<1)|1,1);

        int k=dinic();//k=1说明可以连在其他数后面,不用单独再用一个柱子 
        if(!k) re[++all]=num;
    }
    printf("%d\n",--num);
    for(int i=1;i<=n;++i)
    {
        if(vis[re[i]]) continue;
        int x=re[i];
        vis[x]=1;
        while(x!=0)
        {
            printf("%d ",x);
            x=xia[x];
            vis[x]=1;
        }
        printf("\n");
    }
    return 0;
}

方法2:

匈牙利算法也可以做
给定了柱子数n(最小路径覆盖数)以及放球条件(建边条件),求最多有多少个球(最多有多少个点可以满足这个最小路径覆盖数)。
枚举球的数量。
每来一个球(点)m,枚举1..m-1的每个点i,若i+m满足建边条件(和为完全平方数)则按以下方式建边——
套路拆点,每个点i拆成Xi、Yi,对于一组i、m,连Xi<->Y(m+5000)双向,跑匈牙利算出最大匹配。
根据二分图相关定理:最小路径覆盖数=点数-最大匹配数。
算出当前图的最小路径覆盖k,与给定柱子数比较。
k<n,继续加球。

k=n,可能还有更大的答案,继续加球。

k>n,m-1就是答案,停止加球。

输出路径,遍历1..m-1每个X部点,向其匹配点走,直到无路可走,沿路标记为已遍历。

已遍历过的X部点不再遍历。

代码:

#include<bits/stdc++.h>
#define mem(x) memset(x,0,sizeof(x));
typedef long long ll;
using namespace std;
inline int read(){
   int s=0,w=1;
   char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
   while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();//s=(s<<3)+(s<<1)+(ch^48);
   return s*w;
}
int n,m=0;
const int MAXP=5e3;
const int maxn=200009;
struct node{
    int next,to;
}edge[maxn];
int cnt=0;
int head[maxn];
void add(int u,int v)
{
    edge[++cnt].next=head[u];
    edge[cnt].to=v;
    head[u]=cnt;
}
bool iff(int x)
{
    double t;
    t=sqrt(x);
    return (t==int(t));
}
int vis[maxn];
int match[maxn];
bool maxmatch(int u)
{
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(!vis[v])
        {
            vis[v]=1;
            if(match[v]==0||maxmatch(match[v]))
            {
                match[v]=u;
                match[u]=v;
                return 1;
            }
        }
    }
    return 0;
}
void Print(int x)
{
    x+=MAXP;
    do
    {
        x=x-MAXP;
        cout<<x<<" ";
        vis[x]=1;
        x=match[x];
    }while(x!=0);
    cout<<endl;
}
int main()
{
    cin>>n;
    int ans=0;
    do
    {
        m++;
        int t=m+MAXP;
        for(int i=1;i<m;i++)
        {
            if(iff(i+m))
            {
                add(i,t);
                add(t,i);
            }

        }

        mem(vis); 
        ans+=maxmatch(t);//ans为剩下的柱子,m为原本要花的柱子,m-ans为现在所需要的柱子 
    }while(m-ans<=n);
    m--;
    cout<<m<<endl;
    mem(vis);
    for(int i=1;i<=m;i++)
    {
        if(vis[i]==0)Print(i);
    }
    return 0;
}