【NOIP2017】宝藏(状压DP/爆搜)

问题:

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。

新开发一条道路的代价是:

L×K

L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值。

解:

细数我做这道题的心历路程

首先我看到n这么小 我就先定义 了 $f[i]$ 表示集合 都遍历的最小花费

               $dis[j]$ 表示j的深度 (我开始没看出来要选一棵树,还有深度....)  

            $dp[i|(1<<j-1)]=min(dp[i|1<<j-1],dis[edge[k].st]*edge[k].val+dp[i])$ 其中终点为不在集合内的j 起点为在集合的st

                然后从CCF那里光荣地骗了 50分 .... 

                原因在于 时间复杂度较高 并且dis数组算的有问题

code:

#include<stdio.h>
#include<cstdio> 
#include<algorithm> 
#include<cmath> 
#include<bits/stdc++.h>
using namespace std;
#define maxn 5555
#define maxnn 5000
#define ll long long 
ll dp[maxn];
int dis[maxn];
int tot=0;
int all;
struct node
{
    int st,en;
    int val;
}edge[maxnn];
int n,m;
void add(int a,int b,int c)
{
    edge[++tot].en=b;
    edge[tot].st=a;
    edge[tot].val=c;
}
void dfs(int v)
{
    for(int i=0;i<=n;i++)
    dis[i]=0;
    for(int i=0;i<=all;i++)
    {
        dp[i]=100000000000;
    }
    dis[v]=1;
    dp[1<<v-1]=0;
    for(int i=1<<v-1;i<=all;i++)
    {
        for(int j=1;j<=n;j++)
        {    
            if(((1<<j-1)&i)==0)
            {
                for(int k=1;k<=tot;k++)
                {
                    if((((1<<edge[k].st-1)&i)!=0)&&(edge[k].en==j)&&(dp[i|1<<j-1]>dis[edge[k].st]*edge[k].val+dp[i]))
                    {
                        dp[i|(1<<j-1)]=min(dp[i|1<<j-1],dis[edge[k].st]*edge[k].val+dp[i]);
                        dis[j]=dis[edge[k].st]+1;
                    }
                }
            }
            
        }
    }
}
int main()
{
    cin>>n>>m;
    int x,y,z;
    ll ans=100000000000;
    all=(1<<n)-1;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
        add(y,x,z);
    }
    for(int i=1;i<=n;i++)
    {
        dfs(i);
        ans=min(ans,dp[all]);
    }
    cout<<ans;
}

怎么办?

注意到,我们其实是dis数组算的不对 也就是深度数组算的不对  

并且一个点是由一个点的深度值*VAL权值更新而来  且n这么小 肯定是定义二维数组

所以我们有 的$f[i][j]$ 表示 集合i 并且当前讨论到j 层深度的最小花费

状态转移方程也就出来了

$dp[i|k][j]=min(dp[i|k][j],dp[i][j-1]+(j)*sum k^i)$  其中$sum k^i $ 为深度为j的集合k到集合i的最小距离之和 
k是i的补集的子集 也就是包含了没有被讨论过的点
这里我们就要安利一波枚举子集的方法
for(int i=s,i;i=(i-1)&s)
还有一波快速取出集合元素的方法(x就是原来集合的为1的元素)
for(int i=s;i;i=i-(i&-i))
{
  int x=i&-i;          
}

据说这样和logg2数组搭配更快哦

但是

在经历了一个多小时的卡常优化后 我还是成功被卡常 

luoguAC oj T 75pts  佛了

code:

#include<stdio.h>
#include<cstdio> 
#include<algorithm> 
#include<cmath> 
#include<bits/stdc++.h>
using namespace std;
#define maxn 5555
#define maxnn 5000
#define ll long long 
ll dp[maxn][maxnn];
int sum[maxnn][maxnn];
int mapp[30][30];
int dis[maxnn][maxnn];
int tot=0;
int all;
int n,m;
int logg[1<<14];

inline void init()
{
    for(int i=0;i<=n;i++)
    {
        for(int j=0;j<=all;j++)
        {
            dis[i][j]=10000000;
        }
    }
    for(int j=0;j<=all;j++)
    {
        int s0=j^all;
        for(int i=s0;i;i=(i)-(i&-i))
        {
            int x=i&-i;
            for(int k=j;k;k=k-(k&-k))
            {
                int u=k&-k;

                {
                    dis[logg[x]][j]=min(dis[logg[x]][j],mapp[logg[x]][logg[u]]);
                }
            }
        }    
    }
    for(int j=0;j<=all;j++)
    {
        int s=j^all;
        for(int u=s;u;u=((u-1)&s))
        for(int i=u;i;i=(i)-(i&-i))
        {
            int x=i&-i;
            {
                sum[u][j]+=dis[logg[x]][j];
            }
        }
    }
}
int main()
{
    cin>>n>>m;
    all=(1<<n)-1;
    for(int j=1;j<=n;j++)
    {
        logg[1<<j-1]=j;
    }
    for(int j=0;j<=n;j++)
    {
        for(int i=0;i<=n;i++)
        {
            if(i!=j) mapp[i][j]=100000000;
        }
    }
    int a,b,c;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a,&b,&c);
        mapp[a][b]=min(mapp[a][b],c);
        mapp[b][a]=min(mapp[a][b],c);
    }
    init();
    ll ans=1000000000;
    for(int i=1;i<=n;i++)
    {
    for(int j=0;j<=n;j++)
    {
    for(int i=0;i<=all;i++)
        {
            dp[i][j]=1000000000;
        }
    }
    dp[1<<i-1][0]=0;
    for(int i=0;i<=all;i++)
    {
        for(int j=1;j<=n;j++)
        {
            int s0=(all^i);
            for(int k=s0;k;k=((k-1)&s0))
            {
                dp[i|k][j]=min(dp[i|k][j],dp[i][j-1]+(j)*sum[k][i]);
            }
        }
    }
    for(int j=0;j<=n;j++)
    {
         ans=min(ans,dp[all][j]);
    }
    }
    printf("%lld",ans);
}

我同学帮我玄学优化卡过了


最后我发现了一个神奇的解法    

爆搜

注意到 8!的全排列 其实很小  那就爆搜啊

据说当年Oblack学长 就是爆搜 拿了90pts  %%%

花费时间:0ms

我们需要枚举根节点 然后依次进行转移回溯 

再次证明了爆搜+剪枝的重要性 

code:

//
#include<bits/stdc++.h>
using namespace std;
#define maxnn 30
int tar[maxnn][maxnn];
int cnt=0;
int n,m;
int d[maxnn];
int vis[maxnn];
#define inf 1000000
int tmp=0,tot;
int ans=100000000;
int c[maxnn][maxnn];
int le[maxnn];
int p;
bool cmp(int a,int b){

    return c[p][a]<c[p][b];
}
void dfs(int num,int node)
{
    for(int i=num;i<=cnt;i++)
    {
        if(tot+tmp*le[vis[i]]>ans) return ;
        for(int j=node;j<=d[vis[i]];j++)
        {
            if(!le[tar[vis[i]][j]])
            {
                cnt++;
                vis[cnt]=tar[vis[i]][j];
                tmp-=c[vis[cnt]][tar[vis[cnt]][1]];
                tot+=c[vis[i]][vis[cnt]]*le[vis[i]];
                le[vis[cnt]]=le[vis[i]]+1;
                dfs(i,j+1);
                tot-=c[vis[i]][vis[cnt]]*le[vis[i]];
                le[vis[cnt]]=0;
                tmp+=c[vis[cnt]][tar[vis[cnt]][1]];
                cnt--;
            }
        }
        node =1;
    }
    if(cnt==n)
    {
        if(tot<ans) ans=tot;
        return ;
    }
}
int main()
{
    int x,y,z;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    {
        c[i][j]=inf;
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        if(c[x][y]==inf)
        tar[x][++d[x]]=y,tar[y][++d[y]]=x;
        if(c[x][y]>z)
        {
            c[x][y]=z;
            c[y][x]=z;
        } 
    }
    for(int i=1;i<=n;i++)
    {
        p=i;
        sort(tar[i]+1,tar[i]+1+d[i],cmp);
        tmp+=c[i][tar[i][1]];
    }
    for(int i=1;i<=n;i++)
    {
        tot=0;
        cnt=1;
        vis[1]=i;
        le[i]=1;
        tmp-=c[i][tar[i][1]];
        dfs(1,1);
        le[i]=0;
        tmp+=c[i][tar[i][1]];
    }
    printf("%d",ans);
}
 

 summay:

*通过这道题我觉得CCF的题并不简单  考了状压DP和枚举子集 取集合元素等操作

*爆搜并不是不可过 爆搜非常重要 要多练爆搜




刀剑映出了战士的心。而我的心,漆黑且残破
原文地址:https://www.cnblogs.com/OIEREDSION/p/11415885.html