zoukankan      html  css  js  c++  java
  • 【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和枚举子集 取集合元素等操作

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




    刀剑映出了战士的心。而我的心,漆黑且残破
  • 相关阅读:
    Visual Studio 2005 不能调试的问题
    自学C语言_第一章
    批处理For循环一键Update补丁程序
    小米MiFlash刷机报错售后方法参考
    Civil 3D 2012 CAD安装完成后打开报错“致命错误:Unhandled Delayload "D3DCOMPILER_47.dll"
    Windows查看端口被占用
    vc ++6.0打开或者添加出现错误解决方案
    一天总结
    一天总结
    一天总结
  • 原文地址:https://www.cnblogs.com/OIEREDSION/p/11415885.html
Copyright © 2011-2022 走看看