zoukankan      html  css  js  c++  java
  • 绿色计算大赛决赛 第二阶段 消息传递(斯坦纳树 状压dp+spfa)

    传送门

    Description
    作为公司老板的你手下有N个员工,其中有M个特殊员工。现在,你有一个消息需要传递给你的特殊员工。因为你的公司业务非常紧张,所以你和员工之间以及员工之间传递消息会造成损失。因此,你希望只告诉一部分特殊员工,然后依靠员工之间传递消息,使得所有的特殊员工都能获得要传递的消息,同时使得损失最小。同时,你不关心要传递的消息是否经过了其它员工。求最小的损失。
    Constraint
    补全右侧代码区中的int solve(int N, vector cost_e, vector employees, vector cost_b)函数,完成挑战任务中提出的要求:返回最小的损失。
    如果需要,你可以在solve函数外添加其它代码,但是不要改变Solver类的名字以及solve函数的形式,也不要改变DeliveryCost类的定义。
    函数参数说明如下:
    • int N:员工个数(2 <= N <= 50),员工编号从1到N;
    • vector<DeliveryCost> cost_e:员工之间传递消息的损失,员工cost_e[i].u和cost_e[i].v之间传递消息的损失为cost_e[i].cost。数据保证任意两个员工之间传递消息的损失只出现一次,整个数组长度为N(N-1)/2。(1 <= cost <= 1000)
    • vector<int> employees:特殊员工的编号,个数为M(1 <= M <= 10);
    • vector<int> cost_b:你传递给每个特殊员工的损失,与employees一一对应。(1 <= cost <= 1000)
    Input
    N = 3;
    cost_e = {{1, 2, 2}, {1, 3, 2}, {2, 3, 2}};
    employees = {1, 2};
    cost_b = {1, 1000};
    Output
    3
    题意
    给出n个点的完全图,另外还有一个点,向其中的m个点连有向边。求至少包含这个点和m个点的最小连通图,并输出最小的边权和。
    分析
    首先,问题是求最小边集,边的有向无向其实不重要,所以从单独的点连向m个点的那些有向边,可以直接看成无向边,因此单独的点和那m个点是完全相同的,不用再单独考虑;
    问题转化为:已知N个点M条无向带权边,求一个最小连通图,必须包含其中的K个特殊点。因为要边权的花费最小,所以图中是不应该出现环的,最小连通图一定是一棵树。具有这样性质的树,被定义为斯坦纳树
    斯坦纳树的求解貌似是一个NP问题,做法基本还是暴力,但是因为这道题数据量很小,所以是可做的。推荐一篇大佬分析斯坦纳树的博客
    因为特殊点最多只有11个,所以暴力搜可以从K入手,先大致生成K个点的一个生成树,再看能不能借用其他N-K个点形成的网络的一部分来降低花费。
    具体地来说,把状态定义为$$$(i, (a_1a_2...a_k)_{bin})$$$,表示以$$$i$$$号节点为根的一棵树,$$$a_j$$$为1则表示$$$i$$$至少与第$$$j$$$个特殊点是连通的。
    现在用$$$state$$$简记$$$(a_1a_2...a_k)_{bin}$$$,那么$$$dp[i][state]$$$记录状态$$$(i,state)$$$的最小花费,有下面两个转移方程:
    $$$$$$ egin{align} & dp[i][state]=min_{substatesubset state}{dp[i][substate]+dp[i][state-substate]} \ & dp[i][state]=min{dp[i][state],dp[j][state]+w(i,j)} end{align} $$$$$$ 对这块理解不是很到位,以下可能有不严谨之处。
    第一层转移,对于固定的$$$i$$$,大的$$$state$$$的$$$dp$$$用$$$substate$$$的dp求出并取最小,相当于用两棵小的树可以合并成一棵大一点的树。
    然后第二层转移,所有的生成树大致长什么样都知道了,但很可能还不是最优的,还要进一步减少花费。转移方程的形式其实很像求最短路的形式,对于固定的$$$state$$$,相当于把它们合并为一个新的点,并且就以这个点为起点,在新的图上跑一次最短路,也就是借用其他的点$$$j$$$对原来的边进行了松弛。
    搜索完所有的状态以后,任意一个特殊点$$$X$$$,则$$$dp[X][{1...1}]$$$就是我们要求的斯坦纳树的最小花费。
    比赛的时候想到的错误算法:
    • 网络流:
      • 从单独的点出发->特殊点, 特殊点<->其他点, 特殊点->汇点)。
      • 错误原因:正解的流出量可以大于流入量,导致费用大的边也被选择,或者一条边对答案贡献多次。
    • 缩边
      • 找K个特殊点,两两之间的最短路,缩为一条边,构造新的图,求K个点的最小生成树
      • 错误原因:特殊点之间的最短路可以分叉,缩边会对分叉前的公共部分重复计算。

    代码
    #include <stdio.h>
    #include<queue>
    #include<vector>
    #include <memory.h>
    using std::queue;
    using std::vector;
    /*
     *dp:
     *dp[i][st] 包含第i个点,且至少和state为1的关键点相连的最小花费
     *转移
     *dp[i][st]=Min{dp[i][st],dp[i][st-sub]+dp[i][sub]} 分解为两个
     *dp[i][st]=Min{dp[i][st],dp[j][st]+w(i,j)} i和j有边,关键点外面的部分spfa一下
     */
    #define INF 0x3f3f3f3f
    #define maxn 55
    int g[maxn][maxn];
    int dp[maxn][1 << 12];
    queue<int> help;
    int N,K;
    int vis[maxn];
    struct DeliveryCost {
        int u;
        int v;
        int cost;
    };
    void spfa(int cs){
        while(!help.empty())    {
            int id = help.front();help.pop();
            vis[id] = 0;
            for(int i=1;i<=N;++i){
                if (id == i || g[id][i] == INF)continue;
                if(dp[i][cs]>dp[id][cs]+g[id][i]){
                    dp[i][cs] = dp[id][cs] + g[id][i];
                    if(!vis[i]){
                        vis[i] = 1; help.push(i);
                    }
                }
            }
        }
    }
    
    int solve(int n,
        vector<DeliveryCost> cost_e,
        vector<int> employees,
        vector<int> cost_b) {
        /*********begin*********/
        memset(g, 0x3f, sizeof g);
        memset(dp, 0x3f, sizeof dp);
        //建图
        //员工到员工
        int sz = cost_e.size();
        int tu, tv, tc;
        for(int i=0;i<sz;i++)    {
            tu = cost_e[i].u; tv = cost_e[i].v; tc = cost_e[i].cost;
            g[tu][tv] = tc;
            g[tv][tu] = tc;
        }
        //老板到特殊员工
        K = cost_b.size();
        for(int i=0;i<K;i++)    {
            g[n + 1][employees[i]] = cost_b[i];
            g[employees[i]][n+1] = cost_b[i];
            dp[employees[i]][1 << i] = 0;
        }
        dp[n + 1][1 << K] = 0;
        K++; N=n+1;
        int limit = (1 << K) - 1;
        //第一层转移
        for(int sta=0;sta<=limit;sta++)    {//遍历state
            for(int i=1;i<=N;i++)    {
                for(int s=sta;s;s=(s-1)&sta)    //遍历substate
                    if(dp[i][s]+dp[i][sta-s]<dp[i][sta])
                        dp[i][sta] = dp[i][s] + dp[i][sta - s];
                if (dp[i][sta] < INF)    {//i-sta被松弛,放入队列
                    help.push(i);
                    vis[i] = 1;
                }
            }
            //第二层转移
            spfa(sta);
        }
        //N是特殊点中的一个
        return dp[N][limit];
        /*********end*********/
        
    }
    int main(){
        //测试一下样例
        int n = 3;
        vector<DeliveryCost>cost_e{ { 1, 2, 2 },{ 1, 3, 2 },{ 2, 3, 2 } };
        vector<int> employees{ 1,2 };
        vector<int> cost_b{ 1,1000 };
        printf("%d",solve(n, cost_e, employees, cost_b));
    }
    总结
    虽然过了,还是来算一下复杂度吧
    以每个点为根都有$$$2^k$$$个$$$state$$$,求解每一个都遍历了其所有$$$substate$$$。含$$$x$$$个$$$1$$$的state共$$$C_k^x$$$个,$$$substate$$$数量都是$$$2^x$$$,所以复杂度为
    $$$$$$egin{align} & ncdotsum{C_k^xcdot 2^x}\ =& ncdotsum{C_k^xcdot 1^{k-x}cdot 2^x}\ =& (1+2)^kn =3^kn end{align}$$$$$$ 此外,对每个$$$state$$$,都要跑一遍$$$spfa$$$,因为是稠密图,如果按spfa的最坏情况来看就是:
    $$$$$$egin{align} & 2^kcdot O(spfa)\ =& 2^kcdot O(VE)\ =& 2^kcdot O(ncdot frac{n(n-1)}{2})\ =& 2^kcdot O(n^3)\ end{align}$$$$$$ 最终复杂度为$$$O(3^kn+2^kn^3)=O(2^kn^3)$$$,大约在3e8左右,但是实际上很快,十组样例只跑了14.408秒,可能是因为$$$spfa$$$的复杂度只有$$$O(kE)$$$吧,也有可能是因为数据比较水233。
  • 相关阅读:
    ie调试器
    修改tomcat的部署名称
    Hibernate级联操作
    eclipse 批量 查询 替换
    Hibernate包及相关工具包下载地址
    new Option及用法
    java字符串的判断
    Caused by: java.lang.ClassNotFoundException: javax.persistence.EntityListeners
    【微信公众平台开发】创建自己定义菜单(四)
    fastjson 的简单使用
  • 原文地址:https://www.cnblogs.com/tobyw/p/9955470.html
Copyright © 2011-2022 走看看