1. 程式人生 > >BZOJ4881 線段遊戲(二分圖+樹狀陣列/動態規劃+線段樹)

BZOJ4881 線段遊戲(二分圖+樹狀陣列/動態規劃+線段樹)

  相當於將線段劃分成兩個集合使集合內線段不相交,並且可以發現線段相交等價於逆序對。也即要將原序列劃分成兩個單增序列。由dilworth定理,如果存在長度>=3的單減子序列,無解,可以先判掉。

  這個時候有兩種顯然的暴力。

  將點集劃分成兩部分使內部無邊顯然就是二分圖,於是第一種暴力是在逆序對之間連邊,答案即為2連通塊個數,因為每個連通塊都可以交換黑白點。問題在於暴力連邊是n2的,而顯然實際有用的邊其實只有O(n)條。考慮這樣一種連邊方式:每個點向後綴最小值、字首第一個比他大的點連邊。瞎歸納歸納就可以證明連這些邊就夠了。這個字首第一個比他大的隨便找都行,比如弄個bit。

#include<iostream> 
#include
<cstdio> #include<cmath> #include<cstdlib> #include<cstring> #include<algorithm> using namespace std; #define ll long long #define N 100010 #define P 998244353 char getc(){char c=getchar();while ((c<'A'||c>'Z')&&(c<'a'||c>'z')&&(c<'0'||c>'9'
)) c=getchar();return c;} int gcd(int n,int m){return m==0?n:gcd(m,n%m);} int read() { int x=0,f=1;char c=getchar(); while (c<'0'||c>'9') {if (c=='-') f=-1;c=getchar();} while (c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return x*f; } int n,a[N],pre[N],suf[N],fa[N],tree[N],cnt; inline
void chkmax(int &x,int y){if (a[y]>a[x]) x=y;} inline void chkmin(int &x,int y){if (a[y]<a[x]) x=y;} int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);} void ins(int k,int x){while (k<=n) tree[k]=min(tree[k],x),k+=k&-k;} int query(int k){int s=n;while (k) s=min(s,tree[k]),k-=k&-k;return s;} int main() { #ifndef ONLINE_JUDGE freopen("bzoj4881.in","r",stdin); freopen("bzoj4881.out","w",stdout); const char LL[]="%I64d\n"; #else const char LL[]="%lld\n"; #endif n=read(); for (int i=1;i<=n;i++) fa[i]=i,a[i]=read(),tree[i]=n+1;a[0]=0,a[n+1]=n+1; pre[0]=0;for (int i=1;i<=n;i++) chkmax(pre[i]=pre[i-1],i); suf[n+1]=n+1;for (int i=n;i>=1;i--) chkmin(suf[i]=suf[i+1],i); for (int i=1;i<=n;i++) if (pre[i]!=i&&suf[i]!=i) {cout<<0;return 0;} else { if (suf[i]!=i) fa[find(i)]=find(suf[i]); if (pre[i]!=i) fa[find(i)]=find(query(n+1-a[i])); ins(n+1-a[i],i); } for (int i=1;i<=n;i++) if (find(i)==i) cnt++; int ans=1;while (cnt--) ans=(ans<<1)%P; cout<<ans; return 0; }

  另一種暴力是一個顯然的dp,即設f[i][j]為dp到第i位時,不包含i的集合的最大值是第j個數的方案數。則有f[i][i-1]=Σf[i-1][j] (a[i]>a[j],j<i-1),f[i][j]=f[i-1][j] (a[i]>a[i-1],j<i-1)。將dp陣列看成一維的,顯然就可以用線段樹優化了,即開一棵以a[]為下標的線段樹,對f[i][i-1]線上段樹上查詢字首更新,如果a[i]<a[i-1]就給整個線段樹清零。

#include<iostream> 
#include<cstdio>
#include<cmath>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
#define ll long long
#define N 100010
#define P 998244353
char getc(){char c=getchar();while ((c<'A'||c>'Z')&&(c<'a'||c>'z')&&(c<'0'||c>'9')) c=getchar();return c;}
int gcd(int n,int m){return m==0?n:gcd(m,n%m);}
int read()
{
    int x=0,f=1;char c=getchar();
    while (c<'0'||c>'9') {if (c=='-') f=-1;c=getchar();}
    while (c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
    return x*f;
}
int n,a[N],L[N<<2],R[N<<2],tree[N<<2],lazy[N<<2];
void update(int k){tree[k]=0,lazy[k]=1;}
void down(int k){update(k<<1),update(k<<1|1),lazy[k]=0;}
void up(int k){tree[k]=(tree[k<<1]+tree[k<<1|1])%P;}
void build(int k,int l,int r)
{
    L[k]=l,R[k]=r;
    if (l==r) return;
    int mid=l+r>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
}
void add(int k,int p,int x)
{
    if (L[k]==R[k]) {tree[k]+=x;return;} 
    if (lazy[k]) down(k);
    int mid=L[k]+R[k]>>1;
    if (p<=mid) add(k<<1,p,x);
    else add(k<<1|1,p,x);
    up(k);
}
int query(int k,int l,int r) 
{
    if (L[k]==l&&R[k]==r) return tree[k];
    if (lazy[k]) down(k);
    int mid=L[k]+R[k]>>1;
    if (r<=mid) return query(k<<1,l,r);
    else if (l>mid) return query(k<<1|1,l,r);
    else return (query(k<<1,l,mid)+query(k<<1|1,mid+1,r))%P;
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("bzoj4881.in","r",stdin);
    freopen("bzoj4881.out","w",stdout);
    const char LL[]="%I64d\n";
#else
    const char LL[]="%lld\n";
#endif
    n=read();
    for (int i=1;i<=n;i++) a[i]=read();
    build(1,0,n);add(1,0,2);
    for (int i=2;i<=n;i++)
    {
        int x=query(1,0,a[i]);
        if (a[i]<a[i-1]) update(1);
        add(1,a[i-1],x);
    }
    cout<<tree[1];
    return 0;
}