题解 P3959 【宝藏】

一扶苏一

2018-08-28 17:49:48

题解

提供一篇题解里都没有写到的状压DP。

看到数据范围就知道大概是个状压了。考虑一下怎么设计状态。

看懂题意,题目的意思就是找一棵生成树,使得代价和最小。

考虑在任意时刻,我们关心的只有我们已经把多少点加进生成树了,以及生成树的最大树高是多少。

那么我们就设f_{S,i}为当前生成树已经包含集合S中的点,并且树高是i。

那么状态转移方程就出来了:

f_{S,i}=min{f_{S_0,i-1}+pay},其中满足S_0S的子集,通过S_0加边一定可以联结成Spay是这次加边的花费。

那么怎么判断S_0在转移中是否合法呢?我们设G_S是S能拓展到的边的集合,显然G是可以预处理出来的。

再考虑pay的计算。

ss=S~xor~S_0,即ss是在S但不在S_0中的元素。

这里pay的计算显然是对于每个ss中的元素取S_0中的元素向它连一条最短的边求和后\timesi。

考虑这么做为什么是对的。

对于一个集合SS_0,如果他们之间的边不是被S_0中最大深度的点连接成的,那么一定存在另一个S_1,包含另一种连边的情况使得S_1包含除被最大深度点连接的点以外的所有点,那么通过S_1转移的答案就是最小值,一定是正确的。所以不会漏解。

现在来整理一下思路:

1、预处理出对于每个点的集合所能拓展的点的集合。

2、对于每个点显然把他作为通向地面的点的时候他的深度是0,集合是(1<<i)。那么这样的DP值初始化为0

3、枚举集合,对于每个集合的子集,通过G_S判断该子集是否合法,如果合法,枚举所有需要被连向的点的最小边权求和乘深度,作为答案。

4、答案就是全集在1~n深度的最小值。

下面考虑时间复杂度:

首先考虑我们枚举集合的数量: 我们集合的枚举量显然是全集的子集的子集个数和。乍一看全集有2^n个子集,每个子集有2^n个子集。那么个数是O(4^n)?事实上不是这样。考虑对于所有的的子集,他们的子集个数是O(2^k)个,其中k是子集的元素个数。那么对于大部分的子集,他们的元素个数k都不等于n。显然这个上界太松了。那么如何计算枚举量呢?考虑对于元素个数为x的子集,共有C_{n}^x种方式。每个子集有2^x个子集,那么总的枚举量是C_{n}^x~\times~2^x。应用二项式定理,原式=(2+1)^n=3^n。所以子集的枚举量是O(3^n)

对于每个集合最多有n个元素,每个元素枚举到它的边是O(n)的,所以这里的复杂度是O(n^2)的。

那么总的复杂度是O(3^n~\times~n^2)。大概是七千万左右。考虑对于绝大部分集合跑不满n^2的上界,并且可以进行一些剪枝优化,最终可以通过本题

Code

为了二进制运用方便,读入点的时候减一,所有的点从0编号到n-1。

#include<cstdio>
#include<cstring>
#define rg register
#define ci const int
#define cl const long long int

typedef long long int ll;

namespace IO {
    char buf[50];
}

template<typename T>
inline void qr(T &x) {
    char ch=getchar(),lst=' ';
    while(ch>'9'||ch<'0') lst=ch,ch=getchar();
    while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    if (lst=='-') x=-x;
}

template<typename T>
inline void write(T x,const char aft,const bool pt) {
    if(x<0) {putchar('-');x=-x;}
    int top=0;
    do {
        IO::buf[++top]=x%10+'0';
        x/=10;
    } while(x);
    while(top) putchar(IO::buf[top--]);
    if(pt) putchar(aft);
}

template <typename T>
inline T mmax(const T a,const T b) {if(a>b) return a;return b;}
template <typename T>
inline T mmin(const T a,const T b) {if(a<b) return a;return b;}
template <typename T>
inline T mabs(const T a) {if(a<0) return -a;return a;}

template <typename T>
inline void mswap(T &a,T &b) {T temp=a;a=b;b=temp;}

const int maxn = 15;
const int maxm = 1010;
const int maxt = 1<<maxn;
const int INF = 0x3f3f3f3f;

int n,m,a,b,c,ans=INF;
int frog[maxt][maxn],gorf[maxt],dis[maxn][maxn];

int main() {
    qr(n);qr(m);
    memset(dis,0x3f,sizeof dis);
    for(rg int i=1;i<=m;++i) {
        a=b=c=0;qr(a);qr(b);qr(c);--a;--b;
        dis[b][a]=dis[a][b]=mmin(dis[a][b],c);
    }
    memset(frog,0x3f,sizeof frog);
    int all=(1<<n)-1;
    for(rg int i=1;i<=all;++i) {
        for(rg int j=0;j<n;++j) if(((1<<j)|i) == i) {
            dis[j][j]=0;
            for(rg int k=0;k<n;++k) if(dis[j][k]!=INF) {
                gorf[i]|=(1<<k);
            }
        }
    }
    for(rg int i=0;i<n;++i) frog[1<<i][0]=0;
    for(rg int i=2;i<=all;++i) {
        for(rg int s0=i-1;s0;s0=(s0-1)&i) if((gorf[s0]|i) == gorf[s0]) {
            int _sum=0;
            int ss=s0^i;
            for(rg int k=0;k<n;++k) if((1<<k)&ss) {
                int _temp=INF;
                for(rg int h=0;h<n;++h) if((1<<h)&s0) {
                    _temp=mmin(_temp,dis[h][k]);
                }
                _sum+=_temp;
            }
            for(rg int j=1;j<n;++j) if(frog[s0][j-1]!=INF) {
                frog[i][j]=mmin(frog[i][j],frog[s0][j-1]+_sum*j);
            }
        }
    }
    for(rg int i=0;i<n;++i) ans=mmin(ans,frog[all][i]);
    write(ans,'\n',true);
    return 0;
}

Summary

1、n个元素的子集的子集数量是3^n

2、在进行状态设计时可以考虑对于每个时刻需要知道的信息,以此设计维度

3、预处理大法吼啊!

和上次的相比 更改了两个笔误的地点。重新提交希望管理员给过。