【CCF CSP-202012-4】食材运输

【CCF CSP-202012-4】食材运输

题意概述

T 市有 N 个酒店,这些酒店由 N-1 条双向道路连接,所有酒店和道路构成一颗树。不同的道路可能有不同的长度,运输车通过该道路所需要的时间受道路的长度影响。

在 T 市,一共有 K 种主流食材。莱莱公司有 K 辆车,每辆车负责一种食材的配送,不存在多辆车配送相同的食材。由于不同酒店的特点不同,因此不同酒店对食材的需求情况也不同。莱莱公司每天给这些公司运输食材。对于运输第 i i i种食材的车辆,这辆车可以从任意酒店出发,然后将食材运输到所有需要第 i i i种食材的酒店。假设运输过程中食材的装卸不花时间,运输车足够大使得其能够在出发时就装满全部所需食材,并且食材的重量不影响运输车的速度。为了提高配送效率,这 K 辆车可以从不同的酒店出发。但是由于 T 市对于食品安全特别重视,因此每辆车在配送之前需要进行食品安全检查。鉴于进行食品安全检查的人手不足,最多可以设置 M 个检查点。

现在莱莱公司需要你制定一个运输方案:选定不超过 M 个酒店设立食品安全检查点,确定每辆运输车从哪个检查点出发,规划每辆运输车的路线。

假设所有的食材运输车在进行了食品安全检查之后同时出发,请制定一个运输方案,使得所有酒店的等待时间的最大值最小。酒店的等待时间从运输车辆出发时开始计算,到该酒店所有需要的食材都运输完毕截至。如果一个酒店不需要任何食材,那么它的等待时间为 0。

输入输出格式

输入的第一行包含 3 个正整数 N,M,K,含义见题目描述。接下来 N 行,每行包含 K 个整数。每行输入描述对应酒店对每种食材的需求情况,1 表示需要对应的食材,0 表示不需要。接下来 N-1 行,每行包含 K 个整数 u , v , w u,v,w u,v,w,表示存在一条通行时间为 w w w的双向道路连接 u u u号酒店和 v v v号酒店。保证输入数据是一颗树,酒店从 1 编号到 n,保证 1 < = u , v < = N 1<=u,v<=N 1<=u,v<=N并且 1 < = w < = 1 0 6 1<=w<=10^6 1<=w<=106

数据规模

1 < = N < = 100 , 1 < = M < = K < = 10 1<=N<=100, 1<=M<=K<=10 1<=N<=100,1<=M<=K<=10

算法设计

本题比较难,要 AC 本题需要掌握树状 DP、状压 DP和位运算基础知识,并且需要一定的数学素养。我们需要把这个题目分解成几个子问题来分开求解。

题目给出的酒店构成了一棵树,首先考虑第一个问题:如果以 i i i号酒店为根结点,运输 j j j号食材的车辆从根结点出发,运输到所有需要 j j j号食材的酒店最少需要多少时间?可以观察到这样一个事实,所需最少时间等于以 i i i号酒店为根结点的树中所有边长的 2 倍之和减去从 i i i到达叶结点的最长路径。换句话说,从 i i i到达叶结点的最长路径只走一次,其它边都走两次(想一想为什么?)。通过这个观察,这个问题可以用树状 DP 走一趟 DFS 算法解决。我们把以 i i i号酒店为根结点,运输 j j j号食材到达所有需要 j j j号食材的酒店的最少时间存储到dp1中,总的时间复杂度为 O ( N 2 K ) O(N^2K) O(N2K)

考虑第二个问题,如何从 N N N个酒店中选出 M M M个,使得所有酒店的等待时间的最大值最小?如果 M = = K M==K M==K,那么针对 1 < = k < = K 1<=k<=K 1<=k<=K,所有 m i n d p 1 [ i ] [ k ] , 1 < = i < = N min{dp1[i][k]}, 1<=i<=N mindp1[i][k],1<=i<=N中的最大值即为解,这样你至少能拿到 65%的分数(还有 5%是样例一)。如果 M ! = K M!=K M!=K,问题就变得非常复杂了。我们还需要进行一次动态规划求解,关键问题在于状态如何设计。由于 K < = 10 K<=10 K<=10,非常小,我们可以对其进行状态压缩,利用一个不超过 2 K 2^K 2K的整数表示哪些食材已经被运输完成。整个状态应该是这样的: d p 3 [ i ] [ j ] [ s ] dp3[i][j][s] dp3[i][j][s]表示前 i i i个酒店中选出了 j j j个酒店,运输了 s s s集合中对应的食材所需的最小时间,注意 s s s中如果第 c c c位二进制位为 1,表示第 c c c号食材运输完成。为了降低时间复杂度,我们需要提前计算好利用 i i i号酒店运输 s s s集合中的食材所需的最大时间 d p 2 [ i ] [ s ] dp2[i][s] dp2[i][s]。在计算 dp3 时就可以直接使用 dp2 的值,以免重复计算。那么最后的解就是 m i n d p 3 [ i ] [ M ] [ ( 1 < < K ) − 1 ] , 1 < = i < = N min{dp3[i][M][(1<<K)-1]}, 1<=i<=N mindp3[i][M][(1<<K)1],1<=i<=N。利用滚动数组可以将 dp3 的第一维优化掉。这里还存在最后一个问题,在计算 dp3 时,我们还需要一个循环来枚举 s s s中所有食材的组合,这样四重循环时间复杂度会不会太高?答案是不会,前两重循环的时间复杂度为 O ( N M ) O(NM) O(NM),最后两重循环合在一起,时间复杂度不超过 ∑ i = 1 K i ⋅ C K i < ∑ i = 1 K K ⋅ C K i < K ⋅ 2 K \sum_{i=1} ^{K} i\cdot C_K^i<\sum_{i=1} ^{K} K\cdot C_K^i<K\cdot 2^K i=1KiCKi<i=1KKCKi<K2K(事实上,取 K = 10 K=10 K=10 ∑ i = 1 K i ⋅ C K i \sum_{i=1} ^{K} i\cdot C_K^i i=1KiCKi的值大致等于 5000),因此总的时间复杂度为 O ( N M K 2 K ) O(NMK2^K) O(NMK2K),不会超时。

C代码

#include <bits/stdc++.h>
using namespace std;
using gg = long long;
#define rep(i, a, b, c) for (gg i = (a); i <= (b); i += (c))
#define rrep(i, a, b, c) for (gg i = (a); i >= (b); i -= (c))
constexpr gg MAX = 1e6 + 5;
constexpr gg mod = 1e9 + 7;
constexpr gg INF = 2e18;
constexpr double thre = 1e-7;
constexpr gg nmax = 105, mkmax = 15;
using ag = array<gg, 2>;
gg ti, ni, mi, ki, di, pi, xi, yi;
vector<ag> tree[nmax];
bool food[nmax][mkmax];
gg dp1[nmax][mkmax], dp2[nmax][1 << mkmax], dp3[mkmax][1 << mkmax];
ag dfs(gg root, gg fa, gg k) {
    gg ans = 0, m = 0;
    for (auto& i : tree[root]) {
        if (i[0] == fa) {
            continue;
        }
        ag c = dfs(i[0], root, k);
        if (c == ag{-1, -1}) {
            continue;
        }
        ans += c[0] + i[1] * 2;
        m = max(m, i[1] + c[1]);
    }
    if (m == 0) {
        return food[root][k] ? ag{0, 0} : ag{-1, -1};
    }
    return {ans, m};
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> ni >> mi >> ki;
    rep(i, 1, ni, 1) {
        rep(j, 1, ki, 1) { cin >> food[i][j]; }
    }
    rep(i, 1, ni - 1, 1) {
        gg a, b, c;
        cin >> a >> b >> c;
        tree[a].push_back({b, c});
        tree[b].push_back({a, c});
    }
    rep(i, 1, ni, 1) {
        rep(j, 1, ki, 1) {
            auto ans = dfs(i, -1, j);
            dp1[i][j] = ans[0] - ans[1];
        }
    }
    gg s = (1 << ki) - 1;
    rep(i, 1, ni, 1) {
        rep(j, 1, s, 1) {
            rep(k, 0, ki - 1, 1) {
                if (j & (1 << k)) {
                    dp2[i][j] = max(dp2[i][j], dp1[i][k + 1]);
                }
            }
        }
    }
    memset(dp3, 0x3f, sizeof(dp3));
    dp3[0][0] = 0;
    gg ans = INF;
    rep(i, 1, ni, 1) {
        rep(j, 1, mi, 1) {
            rrep(k, s, 1, 1) {
                for (gg t = k; t != 0; t = (t - 1) & k) {
                    dp3[j][k] = min(dp3[j][k], max(dp2[i][t], dp3[j - 1][t ^ k]));
                }
            }
            ans = min(ans, dp3[j][s]);
        }
    }
    cout << ans << "\n";
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

savkACUNCB: IOnjn

欢迎来到我的世界

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值