【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=1Ki⋅CKi<∑i=1KK⋅CKi<K⋅2K(事实上,取 K = 10 K=10 K=10, ∑ i = 1 K i ⋅ C K i \sum_{i=1} ^{K} i\cdot C_K^i ∑i=1Ki⋅CKi的值大致等于 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;
}