• ZJNU 2471


    COCI 2016/2017 CONTEST #3 - Kronican

    ZJNU 2471 - Kronican


    题意

    (n)个无限体积的杯子,里面都有一些水,Mislav想喝掉所有的水,但他只想喝最多(k)杯水

    所以他需要将这(n)杯水进行合并,将第(i)杯水倒进第(j)杯所需要的花费为(C_{i,j})

    问喝到所有水的最小花费

    限制

    (1leq kleq nleq 20)

    (0leq C_{i,j}leq 10^5)




    思路一(最小树形图)

    由于数据范围只有(20),首先可以枚举出最后剩下的(k)杯水是哪些,最多的情况数为(C_{20}^{10})

    • 可以以(0/1)两种状态表示每个杯子最终状态,存在一个长度为(n)的数组内,以next_permutation来遍历所有(C_n^k)种情况即可,也可通过二进制枚举方法等。

    对于枚举出来的每一个状态,确定了最后“剩下的水杯”是哪些。

    将水杯看作一张有向图中的节点,就表示需要找出一张最小树形图(森林),使得每个“空水杯”顺着箭头走最终都会指向那些“剩下的水杯”。

    又贪心可得,“剩下的水杯”严格为(k)杯时答案最优,所以这可能是一张包含(k)棵有向树的森林,故需要建立一个虚根,让所有“剩下的水杯”指向这个虚根,再对虚根做一遍最小树形图即可。

    • 下图为(n=11,k=3)时的树形图(注意在跑最小树形图算法时应反向建边)

    pic1




    程序一

    #include<bits/stdc++.h>
    using namespace std;
    const int INF=0x3f3f3f3f;
    
    struct Edge{
        int u,v,dis;
        Edge(){}
        Edge(int u,int v,int dis):u(u),v(v),dis(dis){}
    };
    struct Directed_MT{
        int n,m;
        Edge edges[400];
        int vis[25],pre[25],id[25],in[25];
        void init(int n){
            this->n=n;
            m=0;
        }
        void addedge(int u,int v,int dis){
            edges[m++]=Edge(u,v,dis);
        }
        int DirMt(int root){
            int ans=0;
            while(1){
                for(int i=0;i<n;i++)in[i]=INF;
                for(int i=0;i<m;i++){
                    int u=edges[i].u,v=edges[i].v;
                    if(edges[i].dis<in[v]&&u!=v){
                        in[v]=edges[i].dis;
                        pre[v]=u;
                    }
                }
                for(int i=0;i<n;i++){
                    if(i==root)continue;
                    if(in[i]==INF)return -1;
                }
                int cnt=0;
                memset(id,-1,sizeof(id));
                memset(vis,-1,sizeof(vis));
                in[root]=0;
                for(int i=0;i<n;i++){
                    ans+=in[i];
                    int v=i;
                    while(vis[v]!=i&&id[v]==-1&&v!=root){
                        vis[v]=i;
                        v=pre[v];
                    }
                    if(v!=root&&id[v]==-1){
                        for(int u=pre[v];u!=v;u=pre[u]) 
                            id[u]=cnt;
                        id[v]=cnt++;
                    }
                }
                if(cnt==0)break;
                for(int i=0;i<n;i++) 
                    if(id[i]==-1)id[i]=cnt++;
                for(int i=0;i<m;i++){
                    int v=edges[i].v;
                    edges[i].v=id[edges[i].v];
                    edges[i].u=id[edges[i].u];
                    if(edges[i].u!=edges[i].v) 
                        edges[i].dis-=in[v];
                }
                n=cnt;
                root=id[root];
            }
            return ans;
        }
    }MT;
    
    int n,k,a[30];
    int cost[30][30];
    
    int solve()
    {
        MT.init(n+1);
        for(int i=1;i<=n;i++)
        {
            if(a[i]==1)
                MT.addedge(n,i-1,0); //注意编号从0开始,故全部-1
        }
        for(int i=1;i<=n;i++)
            if(a[i]==0)
            {
                for(int j=1;j<=n;j++)
                    if(i!=j)
                        MT.addedge(j-1,i-1,cost[i][j]); //建立反向边
            }
        return MT.DirMt(n);
    }
    
    int main()
    {
        scanf("%d%d",&n,&k);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                scanf("%d",&cost[i][j]);
        for(int i=1;i<=n-k;i++)
            a[i]=0;
        for(int i=n-k+1;i<=n;i++)
            a[i]=1;
        int ans=INF;
        do{
            ans=min(ans,solve());
        }while(next_permutation(a+1,a+1+n));
        printf("%d
    ",ans);
        
        return 0;
    }
    



    思路二(状压DP)

    以二进制存储当前(n)个杯子是否有水

    每一次倒水一定是由“有水的杯子”倒向“有水的杯子”

    故可以枚举状态(S),二进制上以(1)代表有水,以(0)代表无水

    那么对于每一种状态(S),枚举其中两个有水的杯子(i,j),表示此时枚举的是将(i)杯子中的水倒入(j)杯子中

    倒完后,(i)杯子将会成为空杯子,故此时是由状态(S)转移到状态(S xor 2^i)(将(S)中第(i)个位置的(1)变为(0))的,得到状态转移方程为

    [dp[S xor (1<<i)]=min(dp[S xor (1<<i)], dp[S]+cost[i][j]) ]

    由于枚举的(i,j)两位置在(S)内保证为(1)(有水),故(S xor 2^ilt S)一定成立,所以对于状态(S)只需要从大到小枚举即可(自(2^n-1)(0)




    程序二

    #include<bits/stdc++.h>
    using namespace std;
    const int INF=0x3f3f3f3f;
    
    int n,k,cost[25][25];
    int dp[1<<21];
    
    int main()
    {
        scanf("%d%d",&n,&k);
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                scanf("%d",&cost[i][j]);
        dp[(1<<n)-1]=0; //初始状态花费为0
        for(int S=(1<<n)-2;S>=0;S--)
            dp[S]=INF;
        for(int S=(1<<n)-1;S>=0;S--) //枚举状态S
        {
            for(int i=0;i<n;i++) //枚举倒出的杯子
            {
                if(S&(1<<i)) //倒出的杯子中有水
                {
                    for(int j=0;j<n;j++) //枚举倒入的杯子
                    {
                        if(i==j)
                            continue;
                        if(S&(1<<j)) //倒入的杯子中有水
                            dp[S^(1<<i)]=min(dp[S^(1<<i)],dp[S]+cost[i][j]);
                    }
                }
            }
        }
        int ans=INF;
        for(int S=(1<<n)-1;S>=0;S--)
            if(__builtin_popcount(S)==k) //如果剩下的有水杯子个数为k,取一次答案
                ans=min(ans,dp[S]);
        printf("%d
    ",ans);
        
        return 0;
    }
    

  • 相关阅读:
    spoj LCS2
    spoj SUBLEX
    spoj NSUBSTR
    bzoj 2882: 工艺【SAM】
    poj 3294 Life Forms【SA+二分】
    poj 3415 Common Substrings【SA+单调栈】
    poj 2774 Long Long Message【SA】
    poj 2406 Power Strings【kmp】
    poj 1743 Musical Theme【二分+SA】
    hdu 3622 Bomb Game【二分+2-SAT+tarjan】
  • 原文地址:https://www.cnblogs.com/stelayuri/p/13774094.html
Copyright © 2020-2023  润新知