T1. 社团招新
\(20\) 分:\((n \leqslant 10)\)
直接用 dfs 暴力枚举每个人加入分配到哪一个社团中。在 dfs 的过程中维护当前已经得到总评分 \(sum\),以及当前每个社团的人数 \(c_1, c_2, c_3\) 。当找到一种分配方式后,判断 \(\max(c_1, c_2, c_3)\) 是否不超过 \(\frac{n}{2}\) 并用 \(sum\) 更新答案即可。时间复杂度为 \(O(3^n)\) 。
另外 \(35\) 分:\((n \leqslant 200)\)
不难发现上述 dfs 的过程中,存在大量重复的子问题,考虑用动态规划进行优化。
设 f[i][j][k] 表示当前分配了前 \(i\) 人,且 \(c_1 = j\),\(c_2 = k\),\(c_3 = i-j-k\) 时,能够得到的最大评分总和。
枚举 \(i, j, k\),然后分别考虑当前的第 \(i\) 人分配到哪个社团,可以得到转移方程:
求最终的答案时,枚举两个社团的人数 \(j, k\),然后判断三个社团是否都不超过 \(\frac{n}{2}\) 人,取满足这个条件的最大 \(f[n][j][k]\) 即可,时间复杂度为 \(O(n^3)\) 。
另外 \(5\) 分:(特殊性质 \(A\))
此时 \(a_{i,2}=a_{i,3}=0\),相当于仅有一个社团,直接取最大的 \(\frac{n}{2}\) 个 \(a_{i,1}\) 相加即可。
另外 \(10\) 分:(特殊性质 \(B\))
此时 \(a_{i,3} = 0\),相当于仅有两个社团,若不考虑“人数不超过 \(\frac{n}{2}\)” 的限制,那么答案就是 \(\max(a_{i,1}, a_{i, 2})\) 之和,即每个人取最大的社团评分相加。在此基础上考虑限制,设 \(c_1, c_2\) 为不考虑限制时两个社团的人数,那么:
- 若 \(c_1 = c_2\),则此时两个社团人数都是 \(\frac{n}{2}\) 无需做出任何改变。
- 若 \(c_1 > c_2\),则需要将第一个社团中的 \(c_1 - \frac{n}{2}\) 个人分配到第二个,已知每个人从第一个社团转移到第二个社团后,产生的贡献是 \(a_{i,2}-a_{i,1}\),那么取前 \(c_1 - \frac{n}{2}\) 大的贡献相加即可。
- 若 \(c_1 < c_2\),则处理方式与 \(c_1 > c_2\) 类似。
可以使用优先队列或是 sort 排序来得到前几大的贡献,时间复杂度为 \(O(n\log n)\) 。
另外 \(10\) 分:(特殊性质 \(C\))
此时的 \(a_{i, j}\) 是随机生成的,那么对于第 \(j(j \in \{1, 2, 3\})\) 个社团而言,\(a_{i, j}\) 是三个社团评分中最大的概率约 \(\frac{1}{3}\) 。也就是说,如果我们直接对每个人取 \(\max(a_{i, j})\) 相加,那么最终每个社团的人数约等于 \(\frac{n}{3} \leqslant \frac{n}{2}\),必然满足题目的人数限制。时间复杂度为 \(O(n)\) 。
\(100\) 分:
考虑在“特殊性质 \(B\)”的基础上进行优化,依旧是先将每个人最大的社团评分相加。设 \(p\) 表示人数最多的社团编号,那么:
- 若 \(c_p \leqslant \frac{n}{2}\),则另外两个社团人数肯定也都 \(\leqslant \frac{n}{2}\),直接输出答案。
- 若 \(c_p > \frac{n}{2}\),则此时我们需要从第 \(p\) 个社团中选出 \(c_p - \frac{n}{2}\) 个人移动到其他社团。根据贪心的思想,为了让答案减得更少,需要将这些人移动到他们原本次大评分的社团中。移动后,\(c_p = \frac{n}{2}\) 且另外两个社团的人数总和 \(= \frac{n}{2}\),那么也就是说三个社团的人数都是 \(\leqslant \frac{n}{2}\) 的,符合人数限制。
至此,我们便可以像之前一样,用优先队列或者 sort 排序来得到前 \(c_p - \frac{n}{2}\) 大的“次大减最大”的贡献,加到答案中即可,时间复杂度依然是 \(O(n\log n)\) 。
值得一提的是由于每个人只会选择前两大的社团,因此本题还可以做更多个社团的情况。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
void solve() {
int n;
cin >> n;
vector a(n, vector<int>(3));
rep(i, n)rep(j, 3) cin >> a[i][j];
int ans = 0;
vector<int> cnt(3); int p = 0;
vector<int> mx(n), mx2(n), js(n);
rep(i, n) {
rep(j, 3) {
int x = a[i][j];
if (mx[i] < x) {
mx2[i] = mx[i];
mx[i] = x;
js[i] = j;
}
else {
mx2[i] = max(mx2[i], x);
}
}
ans += mx[i];
++cnt[js[i]];
if (cnt[js[i]] > cnt[p]) p = js[i];
}
if (cnt[p] <= n/2) {
cout << ans << '\n';
return;
}
priority_queue<int> q;
rep(i, n) {
if (js[i] != p) continue;
q.push(mx2[i]-mx[i]);
}
int k = cnt[p]-n/2;
while (k--) {
ans += q.top();
q.pop();
}
cout << ans << '\n';
}
int main() {
int t;
cin >> t;
while (t--) solve();
return 0;
}
T2. 道路修复
\(16\) 分:\((k \leqslant 0)\)
此时 \(k \leqslant 0\),不需要考虑乡镇对城市带来的影响。因此可以直接使用 \(\text{Kruskal}\) 算法来计算出最小生成树的边权总和即可。具体地,将 \(m\) 条边按照边权从小到大排序,然后依次考虑连接每一条边后是否会形成环。若不形成环,则加入这条边否则不加入。判断是否形成环,可以使用并查集。时间复杂度为 \(O(m\log m)\) 。
另外 \(32\) 分:(特殊性质 \(A\))
此时 \(c_j = 0\),且对于每个乡镇 \(j\),都存在一个城市 \(i\) 满足 \(a_{j, i} = 0\) 。这说明任何一个乡镇都可以通过一个边权为 \(0\) 的边加入到一个已经连通的图中,即需要付出的代价是 \(0\) 。而如果是中途将乡镇与未连通图中的某个连通块进行连接(前提是这个连通块中存在乡镇边权为 \(0\) 的城市),然后再通过城市之间的边将每个城市进行连通,那么仍然能够发现使所有乡镇与城市连通需要的代价还是 \(0\) 。
容易得到一个贪心的想法:既然任何一个时刻将乡镇与城市进行连通,那么不妨一开始就让所有乡镇与它对应的 \(0\) 代价城市相连接。这样一来,就可以在一开始不费任何代价地产生尽可能多的边权更小的边,这些边都是乡镇与城市之间的边,用于原图中的城市相互之间进行连接。具体地,对于乡镇 \(j\) 进行如下额外的连边:
- 令乡镇的编号为 \(n+j\),城市的编号为 \(i(1 \leqslant i \leqslant n)\) ;
- 那么在 \(n+j\) 与 \(i\) 之间连接一条边权为 \(a_{j, i}\) 的边。
随后,将这些额外的边与原本城市之间的边一同排序,进行一次 \(\text{Kruskal}\) 算法即可计算出答案,注意使用 long long 。设考虑到 \(m \geqslant nk\),且 \(n > k\),时间复杂度依然是 \(O(m\log m)\) 。
另外 \(24\) 分:\((k \leqslant 5)\)
注意到 \(k \leqslant 5\) 很小,可以考虑 \(\text{dfs}\) 枚举每个城镇是否使用。具体的做法与上述“特殊性质 \(A\)” 的做法类似,对于枚举到的每一种使用乡镇的情况,只需要把用到的乡镇对应的边额外加入进来即可,以便产生值更小的边用于城市之间的连通。\(\text{dfs}\) 部分的复杂度是 \(O(2^k)\),总时间复杂度是 \(O(2^km\log m)\) 。
\(100\) 分:
复杂度的瓶颈在于每次都将 \(m\) 条边重新排序了,需要考虑将 \(O(m\log m)\) 的排序从 \(2^k\) 中提取出来。不难发现,\(m \leqslant 10^6\) 虽然很大,但是 \(n \leqslant 10^4\) 比较小,能够构成生成树的边至多有 \(O(n)\) 条。于是我们可以先对城市之间的 \(m\) 条边进行一次 \(\text{Kruskal}\),提取出其中有用的 \(n-1\) 条树边。
这样一来,对于 \(\text{dfs}\) 枚举到的每一种乡镇情况,总边数就是 \(O(nk)\) 级别而非 \(O(m+nk)\),时间复杂度优化到了 \(O(m\log m + 2^knk\log (nk))\)。然而,这样的复杂度可能还不足以通过本题。
考虑到每次还是会对边重新排序,为了避免这种情况,我们其实完全可以在 \(\text{dfs}\) 之前,提前将所有的 \(nk\) 条边与 \(n-1\) 条树边放在一起排序。为每条边标记上这条边来自于哪一个城镇,这样一来,每次只需要枚举 \(nk+n-1\) 条边,根据边的标记来判断对应的乡镇当前是否可以使用,进而判断当前的这条边是否可以选上。时间复杂度进一步降到了 \(O(m\log m + 2^knk)\) 。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
struct UnionFind {
vector<int> d;
UnionFind(int n = 0): d(n, -1) {}
int find(int x) {
if (d[x] < 0) return x;
return d[x] = find(d[x]);
}
bool unite(int x, int y) {
x = find(x); y = find(y);
if (x == y) return false;
if (d[x] > d[y]) swap(x, y);
d[x] += d[y];
d[y] = x;
return true;
}
bool same(int x, int y) {
return find(x) == find(y);
}
int size(int x) {
return -d[find(x)];
}
};
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, m, k;
cin >> n >> m >> k;
vector<tuple<int, int, int, int>> nes, es;
rep(i, m) {
int u, v, w;
cin >> u >> v >> w;
--u; --v;
nes.emplace_back(w, u, v, -1);
}
vector<int> c(k);
rep(j, k) {
cin >> c[j];
rep(i, n) {
int w;
cin >> w;
es.emplace_back(w, i, n+j, j);
}
}
sort(nes.begin(), nes.end());
UnionFind uf(n);
for (auto [w, u, v, _] : nes) {
if (!uf.unite(u, v)) continue;
es.emplace_back(w, u, v, -1);
}
sort(es.begin(), es.end());
ll ans = 1e18;
vector<bool> used(k);
auto Kruskal = [&]() {
ll res = 0;
rep(j, k) if (used[j]) res += c[j];
UnionFind uf2(n+k);
for (auto [w, u, v, id] : es) {
if (id != -1 and !used[id]) continue;
if (!uf2.unite(u, v)) continue;
res += w;
}
return res;
};
auto dfs = [&](auto& f, int i) -> void {
if (i == k) {
ans = min(ans, Kruskal());
return;
}
used[i] = true;
f(f, i+1);
used[i] = false;
f(f, i+1);
};
dfs(dfs, 0);
cout << ans << '\n';
return 0;
}
浙公网安备 33010602011771号