NOIP2017 宝藏

传送门

当时NOIP考试的时候我不会,现在我还是不会。

因为数据范围很小所以能猜到是状压DP。不过平时我们状压DP都是在一个矩阵里面状压DP,不过这次因为我们要打通所有的宝藏屋,那么肯定最后打通的时候通路是一棵树,我们也就是相当于在树上DP。

首先我们先说一种非常强势的做法,状压DP+dfs!(不过以我的智商估计是永远也想不到了)

我们用dp[i]表示当前状态为i的时候的最小代价。我们每次选取一个点作为根节点,之后从这个点开始dfs。每次我们选取一个当前走过的点,之后再选取一个当前没走过的点,如果可以更新的话就更新,之后继续从这个状态dfs。在回溯的时候我们把深度重新设为原来保持的值就可以。

看一下代码,还是比较好理解的……(不过这玩意咋想orz)

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
#include<set>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 50005;
const int INF = 2147483646;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

int dp[1<<15],dis[15],g[15][15],c[15][15],n,m,ans = INF,x,y,w;

void add(int x,int y,int w)
{
    g[x][y] = g[y][x] = w;
    c[x][y] = c[y][x] = 1;
}

void dfs(int x)
{
    rep(i,1,n)
    {
    if(!(x & 1<<(i-1))) continue;
    rep(j,1,n)
    {
        if((1<<(j-1) & x) || !c[i][j]) continue;//c表示i,j之间有没有路,g存路径的长度
        if(dp[1<<(j-1)|x] > dp[x] + dis[i] * g[i][j])
        {
        int temp = dis[j];
        dis[j] = dis[i] + 1;
        dp[1<<(j-1)|x] = dp[x] + dis[i] * g[i][j];
        dfs(1<<(j-1)|x);
        dis[j] = temp;
        }
    }
    }
}

int main()
{
    n = read(),m = read();
    memset(g,63,sizeof(g));
    rep(i,1,m)
    {
    x = read(),y = read(),w = read();
    if(w < g[x][y]) add(x,y,w);
    }
    rep(i,1,n)
    {
    memset(dis,63,sizeof(dis));
    rep(j,1,(1<<n)-1) dp[j] = INF;
    dis[i] = 1,dp[1<<(i-1)] = 0;
    dfs(1<<(i-1));
    ans = min(ans,dp[(1<<n)-1]);
    }
    printf("%d\n",ans);
    return 0;
}

 

之后我们再说另一种方法。因为其实dfs的复杂度难以保证,所以我们考虑最稳定的一种做法,就是状压DP。我们是在一棵树上进行状压的,不过因为这棵树不是定型的,我们可以把一个根节点的深度设为0,之后每次向下一个深度更新的时候,对于当前的一个状态,我们先求它的补集,之后枚举其所有子集进行转移。

首先使用pval表示从点i到集合j的最短距离,之后使用sval表示集合i到集合j的最短距离,也就是集合i中的每一个点到集合j的最短距离之和。这个更新还是很好更新的。

之后dp的方程就是,设当前的状态为s,它的补集是C,枚举C的所有子集j,用i表示当前的层数,那么dp[i+1][s|j] = min(dp[i+1][s|j],dp[i][s] + sval[s][j] * (i+1));

我们并不需要考虑实际上两层之间是怎么相连的,因为我们肯定会枚举到所有的情况,即使当前是按照错误的方法相连,在之后肯定会有一种情况将其更新。

之后就可以做了。答案是min{dp[i][u]},其中u = (1<<n)-1,i取0~n-1

有两个小技巧,一个是计算补集,就是直接^.另外一个是枚举一个集合的所有子集,具体看下面代码实现。

然而其实这个状压DP跑的要比上面的dfs慢一倍……但是稳啊。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
#include<set>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 50005;
const int INF = 10000000;

ll read()
{
    ll ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

ll dp[13][1<<13],g[15][15],pval[13][1<<13],sval[1<<13][1<<13],n,m,ans = 1e15,x,y,w,u;

void initp()
{
    rep(i,1,n)
    rep(j,0,u) pval[i][j] = INF;
    rep(i,1,n)
    rep(j,0,u)
    rep(k,1,n) if(j & (1<<(k-1))) pval[i][j] = min(pval[i][j],g[i][k]*1ll);//更新pval,方法就是枚举j里面的所有点k
}

void inits()
{
    rep(i,0,u)
    rep(j,0,u) sval[i][j] = INF;
    rep(i,0,u)
    {
    int q = i^u;
    for(int s = q;s;s = (s-1) & q)//枚举所有子集并更新
    {
        ll temp = 0;
        rep(j,1,n) if(s & (1<<(j-1))) temp += pval[j][i];
        sval[i][s] = temp >= INF? INF : temp;
    }
    }
}

void init(int x)
{
    rep(i,0,n)
    rep(j,0,u) dp[i][j] = INF;
    dp[0][1<<(x-1)] = 0;//每次改变根节点都要更新
}

int main()
{
    n = read(),m = read(),u = (1<<n)-1;
    rep(i,1,n)
    rep(j,1,n) if(i^j) g[i][j] = INF;//如果不相同就把距离设为INF
    rep(i,1,m) x = read(),y = read(),w = read(),g[x][y] = g[y][x] = min(g[x][y],w);
    initp();
    inits();
    rep(r,1,n)
    {
    init(r);
    rep(i,0,n-1)
    rep(s,0,u)
    {
        if(dp[i][s] == INF) continue;
        int q = s ^ u;
        for(int j = q;j;j = (j-1) & q)
        dp[i+1][s|j] = min(dp[i+1][s|j],dp[i][s] + sval[s][j] * (i+1));//dp转移
    }
    rep(i,0,n-1) ans = min(ans,dp[i][u]);//计算答案
    }
    printf("%lld\n",ans);
    return 0;
}

 

posted @ 2018-09-07 15:21  CaptainLi  阅读(727)  评论(0编辑  收藏  举报