• 【NOI2019】序列


    题面

    https://loj.ac/problem/3158

    题解

    $orz$ $AYSN$

    考场上秒出费用流$64pts$的奆神

    我想这道题的时候在厕所,一开始想到费用流了,但是后来又想了一个$28pts$的背包,感觉背包更靠谱,于是就写背包了。。。

    费用流貌似没那么难,可我却没有想到。果然还是太弱了。

    用流的大小限制取的个数。

    最左边 $S$,左侧 $ABC$ 三个点,右侧 $n$ 个点,左右边$ T$。
    从 $l$ 到 $k$ 枚举成对的点的个数 $i$,$S$ 向 $A$ 和 $B$ 连流量 $l-i$,费用 $0$,向 $C$ 连流量 $i$,费用 $0$。
    $A$ 向 $n$ 个点连流量 $1$,费用 $ai$,类似的,$B$ 连的费用是 $bi$,C是 $ai+bi$。
    最后 $n$ 个点向 $T$ 连流量 $1$ 费用 $0$。 

    update 2020 - 5 - 12

    一篇随便的题解占据热搜,我有点愧疚呢,那就重新写一个 100 分做法吧!

    说一下我在考场上的 28 分的暴力 dp 吧。

    设 $f_{i,j,k,l}$ 为考虑到序列的第 $i$ 个位置,已经选了 $j$ 个 a 数组中的元素,$k$ 个 b 数组中的元素,其中有 $l$ 个位置上,a 数组的元素和 b 数组的元素同时被选上,这种情况下最大的选中的元素之和。

    然后考虑这一次是

    • 不选
    • 只选 $a_i$
    • 只选 $b_i$
    • $a_i,b_i$ 都选

    状态 $O(n^4)$,转移 $O(1)$,总复杂度 $O(n^4)$。


     事实上,$O(n^3)$ 也不难。这道题说是一个序列,但如果把 $(a_i,b_i)$ 看做一个二元组,给定的 ${a_i},{b_i}$ 一个二元组的集合。

    对于一个集合,我们可以把集合中元素按某个关键字排序,使得 “集合” 的无序性得以体现,这样更接近本质。

    我们按 $a_i$ 从大到小排序。考虑构成答案的元素的形式。显然,我们可以发现,若 $i$ 位置没有选 a,一定不存在以后的一个 $j>i$,使得 $j$ 只选了 a。如果这样,把 $j$ 上的 a 换成 $i$ 上的 a,答案不会变的更劣,都选的地方也不会变的更少。这样不仅决策少了,状态也少了。

    所以前面一部分只能做出决策:

    • 只选 a
    • a,b 都选

    后面一部分只能做出决策:

    • 只选 b
    • a,b 都选
    • 都不选

    前一部分只需要记录 $f_{i,j}$,选到了第 $i$ 个,有 $j$ 个是 $a,b$ 都选的。

    后一部分只需要记录 $g_{i,j,k}$,(从后往前)选到了第 $i$ 个,有 $j$ 个只选 b 的,$k$ 个 a,b 都选的,显然有 $(n-i+1-j-k)$ 个都不选的。

    这样枚举前一部分和后一部分的分界,就可以做到 $O(n^3)$ 了。


    到了 $O(n^2)$ 了。必须需要点灵感才能得到进一步的分数了。

    我们发现,按 $a_i$ 或是 $b_i$ 排序是及其愚蠢的,因为我们不仅要枚举 a,还要枚举 b,还要算交集,这样似乎是怎样都无法做到 $O(n^2)$ 的。

    我们按 $a_i+b_i$ 排序。(自然,按 $a_i+b_i$ 排序上一步也是可以做的,但这一步只能按 $a_i+b_i$ 排序)

    如果一个位置,既没有选 a,又没有选 b,那它以后肯定不会有都选的位置了。如果有,换过来,肯定不会变的更劣。

    所以 $a_i+b_i$ 肯定集中出现在一个前缀中(这个前缀不能有空的),我们枚举这个前缀,设为 $i$,此时,$l$ 个都选的位置集中出现在前 $i$ 个位置,可以知道前 $i$ 个位置只空了 $i-k$ 个位置,我们枚举空了 x 个 a,y 个 b,把最小的 x 个 a 和 y 个 b 去掉,再在剩下的序列中把缺的都选最大的加上,就可以了。

    一个问题是如果同时去掉了一个位置上的 a 和 b,那该咋办?事实上,怎么办都可以,因为这样一定是劣的,不管它就行了。

    为了保证复杂度 $O(n^2)$,我们用 4 个链表(分别对前面的 a,前面的 b,后面的 a,后面的 b 开)实现这些操作。添加复杂度 $O(n)$,查询前 $x$ 大 $O(1)$。


    然后是 $O(nlog^2n)$。这一步很显然,因为删的 a 是越来越大的,复活的 b 越来越小,新加的 a 越来越小,少的 b 越来越大,$Delta f(x)$ 单调减,这个函数关于 x 有凸性,我们只需要对找到第一个 $Delta f(x)<0$ 的位置,就是凸函数的最大值。

    用平衡树或是树状数组维护,平衡树常数太大了,只能得 64 分。好像我写的树状数组也才得 80 分。


    最后一步也很显然,我们对每一个 $i$ ,把它取到最大值的点输出出来,发现从 $i$ 到 $i+1$,要么位置不变,要么位置向右平移一位,所以直接判断一下就行了,复杂度 $O(nlog n)$!可以得到 100 分。

    #include<bits/stdc++.h>
    #define N 200500
    #define K (1<<19)
    #define ri register int
    #define LL long long 
    using namespace std;
    
    int n,k,l,t[N],tru[1<<19];
    LL ss[N],tt[N];
    struct node {
      int a,b;
      bool operator < (const node &rhs) const {
        return tru[a]+tru[b]>tru[rhs.a]+tru[rhs.b];
      }
    } v[N];
    
    inline int read() {
      int ret=0,f=0; char ch=getchar();
      while (ch<'0' || ch>'9') f|=(ch=='-'),ch=getchar();
      while (ch>='0' && ch<='9') ret*=10,ret+=ch-'0',ch=getchar();
      return f?-ret:ret;
    }
    
    struct treap {
      int f[K],a[N],cnt;
      LL sum[K];
      inline void clear() {
        for (ri i=1;i<=cnt;i++) {
          for (ri j=a[i];j<K;j+=j&(-j)) f[j]=sum[j]=0;
        }
        cnt=0;
      }
      inline void insert(ri x) {
        a[++cnt]=x;
        for (ri i=x;i<K;i+=(i&(-i))) f[i]++,sum[i]+=tru[x];
      }
      inline void erase(ri x) {
        for (ri i=x;i<K;i+=(i&(-i))) f[i]--,sum[i]-=tru[x];
      }
      inline LL findxsum(ri k) {
        register LL ret=0; register int cur=0;
        for (ri i=18;i>=0;i--) if (f[cur+(1<<i)]<=k ) {
          k-=f[cur+(1<<i)];
          cur+=(1<<i);
          ret+=sum[cur];
        }
        if (cur+1<K) ret+=k*1LL*tru[cur+1];
        return ret;
      }
      inline LL finddsum(ri k) {
        register LL r1=0; register int tot=0;
        for (ri i=K-1;i;i-=(i&(-i))) r1+=sum[i],tot+=f[i];
        return r1-findxsum(tot-k);
      }
    } t1,t2,t3,t4;
    
    inline LL solve(ri x,ri tot,ri i) {
      if (t[x]==i) return tt[x];
      ri y=tot-x;
      t[x]=i; tt[x]=ss[i]-t1.findxsum(x)-t2.findxsum(y)+t3.finddsum(k-i+x)+t4.finddsum(k-i+y);
      return tt[x];
    }
    
    inline LL dp() {
      sort(v+1,v+n+1);
      for (ri i=1;i<=n;i++) ss[i]=ss[i-1]+tru[v[i].a]+tru[v[i].b];
      for (ri i=0;i<=n;i++) t[i]=-1;
      t1.clear(); t2.clear(); t3.clear(); t4.clear();
      for (ri i=1;i<l;i++) t1.insert(v[i].a);
      for (ri i=l;i<=n;i++) t3.insert(v[i].a);
      for (ri i=1;i<l;i++) t2.insert(v[i].b);
      for (ri i=l;i<=n;i++) t4.insert(v[i].b);
      LL ans=0;
      for (ri i=l,pre=0;i<=n && i<=l+(k-l)+(k-l);i++) {
        t1.insert(v[i].a); t2.insert(v[i].b); t3.erase(v[i].a); t4.erase(v[i].b); 
        int tot=i-l,lb=max(max(0,l+tot-k),l-i+tot),ret=lb,rb=min(min(tot-1,k-l-1),i-l-1);
        if (lb>rb+1) continue;
        if (pre<lb) pre=lb;
        if (pre+1<=rb+1 && solve(pre+1,tot,i)>=solve(pre,tot,i)) ret=pre+1; else ret=pre;
        LL cur=solve(ret,tot,i);
        pre=ret;
        if (cur>ans && lb<=rb+1) ans=cur;
      }
      return ans;
    }
    
    int main() {
      int T=read();
      while (T--) {
        n=read(); k=read(); l=read();
        int co=0;
        for (ri i=1;i<=n;i++) v[i].a=read(),tru[++co]=v[i].a;
        for (ri i=1;i<=n;i++) v[i].b=read(),tru[++co]=v[i].b;
        sort(tru,tru+co+1);
        int tn=unique(tru,tru+co+1)-tru-1;
        for (ri i=1;i<=n;i++) v[i].a=lower_bound(tru+1,tru+tn+1,v[i].a)-tru;
        for (ri i=1;i<=n;i++) v[i].b=lower_bound(tru+1,tru+tn+1,v[i].b)-tru;
        printf("%lld
    ",dp());
      }
    }
  • 相关阅读:
    遗传算法
    模拟退火算法
    分支限界法(一)(转)
    (操作Excel 2007以後)Open XML SDK 2.0 for Microsoft Office
    c#接口和抽象类的区别
    抽象工厂模式
    乐在其中设计模式(C#)系列文章索引
    VB6.0 生成 XML方法
    处理一些简单的客户端脚本(2)
    抽象类
  • 原文地址:https://www.cnblogs.com/shxnb666/p/11196623.html
Copyright © 2020-2023  润新知