「SOL」The Hanged Man(HDU)

看到课程标题是 “DP优化”,以为就是斜率优化、单调性之类的……
结果来了些什么奇妙操作


# 题面

给定一棵 \(n\) 个点的树和整数 \(m\),每个点有两类权值 \(a_i,b_i\)

对于每个 \(h=1\sim m\),求树上的一个独立集 \(S\),使得 \(\sum\limits_{i\in S}a_i\le h\),且 \(\sum\limits_{i\in S}b_i\) 最大,输出满足上述条件的 \(S\) 的数量。

数据规模:\(n\le50,m\le5000,1\le a_i\le m,b_i\le 10^6\)


# 解析

题面就描述了一个背包问题,另外有独立集的限制。

如果在原树上直接背包,需要背包合并(由于物品大小并非 \(1\),虽然两个点只会在 LCA 处产生贡献,但是合并的复杂度仍然是 \(\mathcal{O}(m^2)\) 的),复杂度只能是 \(\mathcal{O}(nm^2)\),无法再优化。

话不多说,我们直接来看看怎么用神仙操作来做这道题。

首先我们建出原树的点分树,点分树有两个(重要的)性质,其中一条我们非常熟悉:

  • 树高是 \(\mathcal{O}(\log n)\)

另外一条非常显然但是不常用(至少我没见过哪道题用):

  • 原树上与 \(u\) 相邻的点在点分树上只可能是 \(\mathbf{u}\) 的祖先\(u\) 的后继。

怎么利用这两点来解题?我们考虑按照点分树的 DFS 序 依次决策每个节点是否在独立集中。

这样的话,当我们决策 \(u\) 时,就不需要考虑 \(u\) 的后继是否选入独立集,而只需要考虑 \(\mathbf{u}\) 的祖先 有哪些被选入了独立集中 —— \(u\) 的祖先不多,只有 \(\mathcal{O}(\log n)\) 个,于是可以状压\(f_{u,s,i}\) 表示的状态为「已经决策了 \(u\),从根到 \(u\) 的链上被选入独立集的点是 \(s\)(压缩状态),且当前背包中装了总重为 \(i\) 的物品」,需要维护对应的物品价值最大值以及方案数。

那么 \(s\) 的范围是 \(\mathcal{O}(2^{\log n})=\mathcal{O}(n)\) 的,\(f_{u,s,i}\) 的状态数是 \(\mathcal{O}(n^2m)\) 的。由于是 0-1 背包,可以 \(\mathcal{O}(1)\) 转移,总的复杂度也是 \(\mathcal{O}(n^2m)\) 的。

下面关于一些细节简单说一下:

  • 知道了祖先的选定状态 \(s\),如何判断 \(u\) 是否能加入当前的独立集中?为了保持复杂度必须 \(\mathcal{O}(1)\) 判断:
    • 维护压缩状态 lnk[u] 表示当前点分树的祖先中,哪些是原树上与 \(u\) 相连的节点。
    • 判断只需要判断 lnk[u]\(s\) 是否有交;
    • 维护只需要在 DFS 进入点 \(u\) 时枚举原树上与 \(u\) 相邻的点 \(v\),更新 lnk[v],DFS 退出点 \(u\) 时在 lnk 中撤销 \(u\) 的信息即可。
  • \(f_{u,s,i}\) 中压缩状态 \(s\) 的维护:记 \(u\) 在 DFS 序上的前驱为 \(v\),我们需要从 \(f_v\) 转移到 \(f_u\)
    • 只需要深度小于 \(dep_u\) 的祖先;
    • 就需要把 \(f_{v,s,i}\)\(s\) 中深度大于等于 \(dep_u\) 的部分删去;
    • 具体的,在 DFS 退出点 \(u\) 时,对每个 \(s\ge 2^{dep_u}\),将 \(f_{u,s,i}\) 贡献到 \(f_{u,s-2^{dep_u},i}\)
    • 这样的效果就是递归到当前层时,有用的 \(s\) 的位数不超过 \(dep_v-1\)

大概是一个不错的处理独立集的方法……


# 源代码

/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

inline int rin(int &r){
    int b=1,c=getchar();r=0;
    while(c<'0' || '9'<c) b=c=='-'?-1:b,c=getchar();
    while('0'<=c && c<='9') r=(r<<1)+(r<<3)+(c^'0'),c=getchar();
    return r*=b;
}
const int N=55,M=5005;
#define con(type) const type &

struct Data{
    int key;long long cnt;
    friend void maxc(Data &a,con(Data)b){
        if(a.key<b.key) a=b;
        else if(a.key==b.key) a.cnt+=b.cnt;
    }
    Data operator +(con(int)d)const{return (Data){key+d,cnt};}
}f[2][64][M];
struct Graph{
    int head[N],to[N<<1],nxt[N<<1],ncnt;
    void init(con(int)sz){for(int i=1;i<=sz;i++)head[i]=0;ncnt=0;}
    void addEdge(con(int)u,con(int)v,con(bool)typ=true){
        ncnt++;
        to[ncnt]=v,nxt[ncnt]=head[u],head[u]=ncnt;
        if(typ) ncnt++,to[ncnt]=u,nxt[ncnt]=head[v],head[v]=ncnt;
    }
    inline int operator [](con(int)u){return head[u];}
}gr;

int ncas,n,m,r_wei,r_root,r_tot,bagsiz,ndfn;
int ara[N],arb[N],nxtto[N];
bool ban[N];

int toggleSize(con(int)u,con(int)fa){
    int sizu=1;
    for(int it=gr[u];it;it=gr.nxt[it])
        if(gr.to[it]!=fa && !ban[gr.to[it]])
            sizu+=toggleSize(gr.to[it],u);
    return sizu;
}
int findCenter(con(int)u,con(int)fa){
    int sizu=1,mxv=0;
    for(int it=gr[u];it;it=gr.nxt[it]){
        int v=gr.to[it],sizv;
        if(v==fa || ban[v]) continue;
        sizv=findCenter(v,u);
        sizu+=sizv,mxv=max(mxv,sizv);
    }
    mxv=max(mxv,r_tot-sizu);
    if(r_wei>mxv) r_wei=mxv,r_root=u;
    return sizu;
}
void dacDFS(con(int)u,con(int)dep){
    // # Marks
    for(int it=gr[u];it;it=gr.nxt[it]) nxtto[gr.to[it]]|=1<<dep;
    ban[u]=true;
    // # DP
    int I=(++ndfn)&1;
    for(int s=0,ss=1<<(dep+1);s<ss;s++)
        for(int i=0,ii=m;i<=ii;i++)
            f[I][s][i].key=-1,f[I][s][i].cnt=0;
    if(dep){
        for(int s=0,ss=1<<dep;s<ss;s++)
            for(int i=0;i<=bagsiz;i++)
                f[I][s][i]=f[!I][s][i];
        int ii=min(bagsiz,m-ara[u]);
        for(int s=0,ss=1<<dep;s<ss;s++){
            if(nxtto[u]&s) continue;
            for(int i=0;i<=ii;i++)
                if(~f[!I][s][i].key)
                    maxc(f[I][s|(1<<dep)][i+ara[u]],f[!I][s][i]+arb[u]);
        }
    }
    else{
        f[I][0][0].key=0,f[I][0][0].cnt=1;
        f[I][1][ara[u]].key=arb[u],f[I][1][ara[u]].cnt=1;
    }
    bagsiz=min(m,bagsiz+ara[u]);
    // # Transport
    for(int it=gr[u];it;it=gr.nxt[it]){
        int v=gr.to[it],sizv;
        if(ban[v]) continue;
        sizv=toggleSize(v,0);
        r_tot=sizv,r_wei=n+1,findCenter(v,0);
        int rtv=r_root;
        // printf("%d -> %d\n",u,v);
        dacDFS(rtv,dep+1);
    }
    // # Roll-back
    for(int it=gr[u];it;it=gr.nxt[it]) nxtto[gr.to[it]]^=1<<dep;
    ban[u]=false;
    // # Toggle-DP
    I=ndfn&1;
    for(int s=1<<dep,ss=1<<(dep+1);s<ss;s++)
        for(int i=0;i<=bagsiz;i++)
            maxc(f[I][s^(1<<dep)][i],f[I][s][i]);
}
int main(){
    rin(ncas);
    for(int cas=1;cas<=ncas;cas++){
        rin(n),rin(m);
        // # Clear
        gr.init(n);
        bagsiz=ndfn=0;
        // # Read & Init-build
        for(int i=1;i<=n;i++) rin(ara[i]),rin(arb[i]);
        for(int i=1,u,v;i<n;i++) gr.addEdge(rin(u),rin(v));
        // # Calculate
        r_tot=n,r_wei=n+1,findCenter(1,0);
        int rt=r_root;
        dacDFS(rt,0);
        // # Print
        printf("Case %d:\n",cas);
        int I=ndfn&1;
        for(int i=1;i<=m;i++) printf("%lld%c",f[I][0][i].cnt,i==m?'\n':' ');
    }
    return 0;
}

THE END

Thanks for reading!

谁留下 烽火连天照夜的勾划
又是谁在传说中寻他
千年后 挥兵破阵 不过一刹那
血雨腥风吻过了伤疤

——《山河令》By 忘川风华录 / 星尘

> Link 山河令-网易云

posted @ 2021-03-03 23:03  Lucky_Glass  阅读(146)  评论(0编辑  收藏  举报
TOP BOTTOM