[网络流24题]最长不下降子序列[题解]
最长不下降子序列
题目大意
在给出的长为 \(n\) 的序列中完成以下三个任务
-
计算其最长不下降子序列的长度 \(s\)
-
如果每个元素只允许被取出一次,求最多可以取出多少个长度为 \(s\) 的不下降子序列
-
如果序列中第一个和最后一个元素允许被多次取出,求最多可以取出多少个长度为 \(s\) 的不下降子序列
题目分析
第一问
第一个问题是比较容易解决的,由于 \(n\) 很小,所以我们可以考虑一个时间复杂度为 \(O(n^2)\) 的算法。
设 \(f[i]\) 表示以 \(a[i]\) 结尾的最长不下降子序列的长度,则在遍历 \(i-n\) 的过程中,我们可以查询 \(j<i\) 中满足 \(a[j]<=a[i]\) 的 \(f[j]\) 的最大值,而 \(f[i]\) 就等于它的值加一。
最后再在\(f[i]\)中寻找最大值即可。
具体代码如下:
//解决问题一,由于数据很小暴力n^2即可解决
for(register int i=1; i<=n; i++) a[i]=read();
for(register int i=1; i<=n; i++) {
for(register int j=1; j<i; j++)
if(a[i]>=a[j]) f[i]=max(f[i],f[j]);
f[i]++;
ans=max(f[i],ans); //更新答案
}
第二问
第二问中,要求我们不能把每个元素重复取出,我们很自然的想到了网络流的拆点操作可以解决此类要求,于是考虑网络流算法。
将一个点 \(i\) 拆成 \(i\) 与 \(i'\) ,在它们之间连上一条容量为 \(1\) 的边,表示每个点只能被取用一次。
然后,为了满足我们找到的序列都是不下降子序列,再联想到第一问中处理出来的 \(f\) 数组,则我们可以对 \(f[i]=f[j]+1\) 且 \(a[j]\leq a[i]\) 的点在它们之间建立一条容量为 \(1\) 的边将 \(j'\) 和 \(i\) 连接起来。
最后,再建立超级源点和超级汇点,用超级源点向每个满足 \(f[i]=1\) 的点连边,每个满足 \(f[i]=s\) 的点再向超级汇点连边。
以上便是建图全过程,最后再跑一遍网络最大流即可。
代码如下:
//解决问题二
//建图
//超级源点设为0,超级汇点设为2*n+1
s=0,t=2*n+1;
//将1~n全部拆为两个点,点i的拆点为i+n
//如果f[i]为1,则向超级源点连一条边
//如果f[i]为s,则向超级汇点连一条边
for(register int i=1;i<=n;i++){
if(f[i]==1) Add(s,i,1),Add(i,s,0);
if(f[i]==ans) Add(i+n,t,1),Add(t,i+n,0);
}
//设节点i的拆点为i'
//则若a[j]<=a[i]&&f[j]+1=f[i]则j'向i连接一条边
for(register int i=1;i<=n;i++)
for(register int j=1;j<i;j++)
if(a[i]>=a[j]&&f[i]==f[j]+1) Add(j+n,i,1),Add(i,j+n,0);
//连接自己
for(register int i=1;i<=n;i++) Add(i,i+n,1),Add(i+n,i,0);
第三问
明白了第二问,第三问就比较容易了,只用把超级源点连向节点 \(1\) 的边的容量改为正无穷,节点 \(n\) 连向超级汇点的边的容量同样改为正无穷,还要注意更换他们内部自身的连边也改为正无穷即可。
CODE
#include <bits/stdc++.h>
using namespace std;
const int N=5e2+10;
int n,s,t,ans,maxf1,maxf2;
int a[N],f[N]; //f[i]表示以a[i]结尾最长不下降子序列的长度
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();
return s*w;
}
int deep[2*N*N],head[2*N*N];
int tot=-1,v[2*N*N],w[2*N*N],nex[2*N*N],first[2*N*N];
inline void Add(int x,int y,int z)
{
nex[++tot]=first[x];
first[x]=tot;
v[tot]=y,w[tot]=z;
}
inline bool BFS()
{
for(register int i=0;i<=2*n+1;i++) deep[i]=-1;
queue<int> q;
q.push(s);
deep[s]=0;
while(!q.empty()){
int now=q.front(); q.pop();
for(register int i=first[now];i!=-1;i=nex[i])
if(w[i]>0&&deep[v[i]]==-1) deep[v[i]]=deep[now]+1,q.push(v[i]);
}
return deep[t]!=-1;
}
inline int DFS(int S,int T,int flow)
{
if(S==T) return flow;
int res=0;
for(register int& i=head[S];i!=-1;i=nex[i]){
if(deep[v[i]]==deep[S]+1){
int temp=DFS(v[i],T,min(flow,w[i]));
w[i]-=temp;
w[i^1]+=temp;
flow-=temp;
res+=temp;
if(!flow) return res;
}
}
return res;
}
inline void dinic1() //第一问
{
while(BFS()){
for(register int i=0;i<=2*n+1;i++) head[i]=first[i];
maxf1+=DFS(s,t,1e9);
}
}
inline void dinic2() //第二问
{
while(BFS()){
for(register int i=0;i<=2*n+1;i++) head[i]=first[i];
maxf2+=DFS(s,t,1e9);
}
}
int main()
{
n=read();
for(register int i=0;i<=2*n+1;i++) first[i]=-1;
//解决问题一,由于数据很小暴力n^2即可解决
for(register int i=1; i<=n; i++) a[i]=read();
for(register int i=1; i<=n; i++) {
for(register int j=1; j<i; j++)
if(a[i]>=a[j]) f[i]=max(f[i],f[j]);
f[i]++;
ans=max(f[i],ans); //更新答案
}
//解决问题二
//建图
//超级源点设为0,超级汇点设为2*n+1
s=0,t=2*n+1;
//将1~n全部拆为两个点,点i的拆点为i+n
//如果f[i]为1,则向超级源点连一条边
//如果f[i]为s,则向超级汇点连一条边
for(register int i=1;i<=n;i++){
if(f[i]==1) Add(s,i,1),Add(i,s,0);
if(f[i]==ans) Add(i+n,t,1),Add(t,i+n,0);
}
//设节点i的拆点为i'
//则若a[j]<=a[i]&&f[j]+1=f[i]则j'向i连接一条边
for(register int i=1;i<=n;i++)
for(register int j=1;j<i;j++)
if(a[i]>=a[j]&&f[i]==f[j]+1) Add(j+n,i,1),Add(i,j+n,0);
//连接自己
for(register int i=1;i<=n;i++) Add(i,i+n,1),Add(i+n,i,0);
dinic1();
//对于第三问,只需将1,n两个数
for(register int i=0;i<=2*n+1;i++) first[i]=-1;
for(register int i=0;i<=tot;i++) nex[i]=0;
for(register int i=0;i<=tot;i++) v[i]=0;
for(register int i=0;i<=tot;i++) w[i]=0;
tot=-1;
for(register int i=1;i<=n;i++){
if(f[i]==1){
if(i==1) Add(s,i,1e9),Add(i,s,0);
else Add(s,i,1),Add(i,s,0);
}
if(f[i]==ans){
if(i==n) Add(i+n,t,1e9),Add(t,i+n,0);
else Add(i+n,t,1),Add(t,i+n,0);
}
}
for(register int i=1;i<=n;i++)
for(register int j=1;j<i;j++)
if(a[i]>=a[j]&&f[i]==f[j]+1) Add(j+n,i,1),Add(i,j+n,0);
for(register int i=1;i<=n;i++){
if(i==1||i==n) Add(i,i+n,1e9),Add(i+n,i,0);
else Add(i,i+n,1),Add(i+n,i,0);
}
dinic2();
if(n==1) maxf2=1;
printf("%d\n%d\n%d\n",ans,maxf1,maxf2);
return 0;
}