独立集 最长上升子序列

NKOJ 3500 独立集

这里写图片描述

输入格式

输入包含两行,第一行为 N,
第二行为 1 到 N 的一个全排列

输出格式

输出包含两行,第一行输出最大独立集的大小,第二行从小到大输出一定在最大独立集 的点的编号。

样例输入

3
3 1 2

样例输出

2
2 3

数据规模

30%的数据满足 N<=16
60%的数据满足 N<=1,000
100%的数据满足 N<=100,000


首先不要被迷惑了,虽然“最大独立集”容易让我们想到二分图匹配,但是这道题的正解与二分图匹配一点关系也没有!

首先一定要知道对于给定的一个排列,按照题目中的方式建出的图是怎么样的。根据冒泡排序的实现过程,可以知道只有逆序对才会有边相连。

所以是树状数组or归并排序?虽然时间复杂度O(nlogn)看起来很契合数据规模,但是发现知道逆序对的数量并没有什么用,尤其是第二问,根本没有关系。

考虑独立集的特征。由于只要有逆序对,那么就有边相连;只要有边相连,就不是独立集,所以独立集里的点一定是在给出的排列里的上升子序列。由于是最大独立集,那么最大独立集的大小就是最长上升子序列的长度。这可以通过O(nlogn)的算法实现。

考虑第二问。实在想不出就可以暴力枚举每个点,判断去掉它会不会使得最长上升子序列的长度减小,如果减小就说明它是必须的,反之则不是。然而这样的时间复杂度为O(n2logn),会超时。

再考虑必须在最大独立集的点的特征。标程给的方法比较机智,这样的点必然满足三个特征:

1.它处在一个最长上升子序列中(废话)
2.在所有能处在一个最长上升子序列的点当中,以它为结尾的最长上升子序列的长度唯一
3.在所有能处在一个最长上升子序列的点当中,以它为开头的最长上升子序列的长度唯一

对于1,正反两次最长上升子序列算法就搞定了,只需要在倒着跑的时候把原排列中的每个元素改为负数即可。

对于2,3,注意到2和3是等价的,因为最长上升子序列的长度唯一。这是比较难理解的一点,下面给出两种解释:

第一种说得比较简单,来自PWJ大佬:

“以它为结尾的最长上升子序列的长度唯一”,长度其实表示了位置,也就是说在最长上升子序列中,这个位置只有一种选择。如果这个长度不是唯一的,说明在这个位置上有多种选择,那么任何一种选择可以互相替代,就都不是必须的。

第二种是我的解释,稍微啰嗦一点:

首先容易理解,如果这个长度不是唯一的,那么它一定不是必须的。因为这说明了有多种不同的方案。
然后解释为什么长度唯一就一定是必须的。采用反证法,假设以某个点为结尾的长度唯一,但是它却不是必须的。什么样的点不是必须的呢?就是存在一个最长上升子序列,满足这个点不在它当中。那么对于这一个最长上升子序列,在对应位置上存在一个与它不同的点,那么就与“以它为结尾的长度唯一”的条件矛盾了。


然后代码就比较好写了:

#include<stdio.h>
#include<algorithm>
using namespace std;
#define MAXN 100005

int N,p[MAXN],Ans=1,f[MAXN],g[MAXN],Cnt[MAXN],ff[MAXN],gg[MAXN];

int main()
{
    int i,k,j,tot=0;

    scanf("%d",&N);
    for(i=1;i<=N;i++)scanf("%d",&p[i]);

    for(i=1;i<=N;i++)g[i]=1e9;
    f[1]=1;g[1]=p[1];
    for(i=2;i<=N;i++)
    {
        j=lower_bound(g+1,g+Ans+1,p[i])-g;
        f[i]=j;
        g[j]=min(g[j],p[i]);
        Ans=max(f[i],Ans);
    }

    printf("%d\n",Ans);
    int Tmp=Ans;

    Ans=1;
    for(i=1;i<=N;i++)gg[i]=1e9;
    for(i=N;i>=1;i--)
    {
        j=lower_bound(gg+1,gg+Ans+1,-p[i])-gg;
        ff[i]=j;
        gg[j]=min(gg[j],-p[i]);
        Ans=max(ff[i],Ans);
    }

    for(i=1;i<=N;i++)if(f[i]+ff[i]-1==Tmp)Cnt[f[i]]++;

    for(i=1;i<=N;i++)if(f[i]+ff[i]-1==Tmp&&Cnt[f[i]]==1)printf("%d ",i);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值