21.12 杂题
12.9
Weather Report「ICPC World Finals 2015」
一开始读错了,以为是每种天气一个,其实是对于 \(4^n\) 种天气各一个。直接算出每种序列出现的权重,这可以直接枚举每种天气的天数然后可重接排列数合并。然后考虑哈夫曼树,每次找到两个最小的合并起来。这题对于一堆有多个的,如果是偶数显然是两两合并,是奇数就先干掉一个然后再两两合并。
#include <bits/stdc++.h>
using namespace std;
int n, m = 4;
double p[5], q[5] = {0, 1, 2, 3, 3}, ans;
priority_queue<pair<double, long long>, vector<pair<double, long long>>, greater<pair<double, long long>>> v;
long long calc(int a, int b, int c, int d) {
double ret = 1;
for (int i = 1; i <= n; i++) ret *= i;
for (int i = 1; i <= a; i++) ret /= i;
for (int i = 1; i <= b; i++) ret /= i;
for (int i = 1; i <= c; i++) ret /= i;
for (int i = 1; i <= d; i++) ret /= i;
return (long long)(ret);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= m; i++) scanf("%lf", p + i);
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
for (int k = 0; k <= n; k++) {
for (int l = 0; l <= n; l++) {
if (i + j + k + l == n) {
v.push(make_pair(pow(p[1], i) * pow(p[2], j) * pow(p[3], k) * pow(p[4], l), calc(i, j, k, l)));
}
}
}
}
}
while (v.size()) {
pair<double, long long> now = v.top(); v.pop();
if (!v.size() && now.second == 1) continue;
if (now.second > 1) {
if (now.second & 1) v.push(make_pair(now.first, 1)), now.second--;
ans += now.first * now.second;
v.push(make_pair(now.first * 2, now.second / 2));
} else {
pair<double, long long> nxt = v.top(); v.pop();
ans += now.first + nxt.first;
if (nxt.second > 1) v.push(make_pair(nxt.first, nxt.second - 1));
v.push(make_pair(now.first + nxt.first, 1));
}
}
printf("%.6lf\n", ans);
return 0;
}
Journey from Petersburg to Moscow「NEERC2017」
这种题一般先考虑你得到了一个最优方案,然后你只用付前 \(k\) 大的代价,也就是说小于第 \(k\) 大的代价都不需要管,那么我们不妨把所有代价减去一个第 \(k\) 大的代价,最后再加上其的 \(k\) 倍即可。
然后考虑枚举哪个是第 \(k\) 大。然后减去其跑最短路,这时我们需要考虑最短路是否恰好是 \(k\) 条路组成。但这显然太麻烦了,这种题一般都是找一些性质然后得到一个更简单的方法。考虑直接对算出来的所有答案取 min,然后就发现这东西对了。具体证明就分类讨论一下比第 \(k\) 大小或者大的情况。很多曼哈顿距离的那种题也可以用类似的方法做。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 30005;
const ll inf = 0x3f3f3f3f3f3f3f3f;
template <typename T>
void read(T &x) {
T flg = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') flg = -1;
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= flg;
}
struct zcr{
ll dis;
int num;
};
bool operator < (zcr a, zcr b) {
return make_pair(a.dis, a.num) < make_pair(b.dis, b.num);
}
zcr operator + (zcr a, zcr b) {
return zcr{a.dis + b.dis, a.num + b.num};
}
struct EDGE{
int u, v, w;
}e[maxn];
struct node{
int pre, to, val, zz;
}edge[maxn << 1];
int head[maxn], tot = 1;
ll ans = inf;
zcr dis[maxn];
int N, M, K;
bool vis[maxn];
priority_queue<pair<zcr, int>, vector<pair<zcr, int>>, greater<pair<zcr, int>>> q;
void dij() {
for (int i = 1; i <= N; i++) dis[i] = zcr{inf, 0}, vis[i] = 0;
dis[1] = zcr{0, 0}; q.push(make_pair(dis[1], 1));
while (q.size()) {
int u = q.top().second; q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = edge[i].pre) {
int v = edge[i].to;
if ((dis[u] + zcr{edge[i].val, 1}) < dis[v]) {
dis[v] = dis[u] + zcr{edge[i].val, 1};
q.push(make_pair(dis[v], v));
}
}
}
}
void add_edge(int u, int v, int w) {
edge[++tot] = node{head[u], v, w, w};
head[u] = tot;
}
int main() {
read(N); read(M); read(K);
for (int i = 1; i <= M; i++)
read(e[i].u), read(e[i].v), read(e[i].w), add_edge(e[i].u, e[i].v, e[i].w), add_edge(e[i].v, e[i].u, e[i].w);
dij();
if (dis[N].num <= K) ans = dis[N].dis;
for (int i = 1; i <= M; i++) {
for (int j = 2; j <= tot; j++) {
edge[j].val = max(edge[j].zz - e[i].w, 0);
}
dij();
ans = min(ans, dis[N].dis + 1ll * e[i].w * K);
}
printf("%lld\n", ans);
return 0;
}
Bipartite Blanket「CERC2016」
赛时以为 Hall 定理是个很复杂的东西,然后经过学长点拨发现好像是个挺 naive 的玩意。
首先先不考虑第一个条件,我们发现对于一个点集它可以,仅当其左部点和整个图的右部点有完美匹配,右部点同理。然后根据惯例考虑必要条件是否充分,然后对边调整一下发现真的充分了。于是可以 \(O(2^n n)\) 排序双指针了。至于怎么判断,用 Hall 定理即可。
简单介绍 Hall 定理。还是考虑充分必要条件。显然对于每个子集,其连出去的边连到的点所构成的集合的大小一定不小于当前子集的大小。然后发现这必要条件太强了,直接充分。证明可以考虑反正法。这样乍一看还要 \(O(3^n)\) 枚举子集。其实不需要,稍微修改,对于每个大小为 \(|S|-1\) 的子集存在完美匹配,并且 \(S\) 满足条件。这样就可以 \(O(2^n n)\) 了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template <typename T>
void read(T &x) {
T flg = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') flg = -1;
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= flg;
}
ll n, m, T;
ll a[25][25];
char s[25];
ll v[25], w[25], id[1 << 20];
ll zz[25], cc[25];
bool mark[1 << 20];
vector<ll> v1, v2;
ll ans;
int main() {
read(n); read(m);
for (ll i = 1, j = 1; i < (1 << 20); i <<= 1, j++) id[i] = j;
for (ll i = 1; i <= n; i++){
scanf("%s", s + 1);
for (ll j = 1; j <= m; j++)
a[i][j] = s[j] - '0', zz[i] |= a[i][j] << (j - 1), cc[j] |= a[i][j] << (i - 1);
mark[1 << (i - 1)] = zz[i] > 0;
}
for (ll i = 1; i <= n; i++) read(v[i]);
for (ll i = 1; i <= m; i++) read(w[i]);
read(T); v1.push_back(0); v2.push_back(0);
for (ll i = 1; i < (1 << n); i++) {
ll now = 0, rr = 0;
if (__builtin_popcount(i) == 1 && mark[i]) {
v1.push_back(v[id[i]]);
continue;
}
mark[i] = 1;
for (ll j = 0; j < n; j++) {
if (i & (1 << j)) {
rr |= zz[j + 1];
mark[i] &= mark[i ^ (1 << j)];
now += v[j + 1];
}
}
mark[i] &= __builtin_popcount(i) <= __builtin_popcount(rr);
if (mark[i]) {
v1.push_back(now);
}
}
memset(mark, 0, sizeof(mark));
for (ll i = 1; i <= m; i++) mark[1 << (i - 1)] = cc[i] > 0;
for (ll i = 1; i < (1 << m); i++) {
ll now = 0, rr = 0;
if (__builtin_popcount(i) == 1 && mark[i]) {
v2.push_back(w[id[i]]);
continue;
}
mark[i] = 1;
for (ll j = 0; j < m; j++) {
if (i & (1 << j)) {
rr |= cc[j + 1];
mark[i] &= mark[i ^ (1 << j)];
now += w[j + 1];
}
}
mark[i] &= __builtin_popcount(i) <= __builtin_popcount(rr);
if (mark[i]) {
v2.push_back(now);
}
}
sort(v1.begin(), v1.end()); sort(v2.begin(), v2.end()); reverse(v2.begin(), v2.end());
for (ll i = 0, j = 0; i < (ll)v2.size(); i++) {
while (j < (ll)v1.size() && v1[j] + v2[i] < T) j++;
if (j < (ll)v1.size()) ans += v1.size() - j;
}
printf("%lld\n", ans);
return 0;
}
12.12
Outer space invaders「CERC 2014」
根据数据范围猜测 dp。先思考过只记一维时间,然后发现这样不太能知道到底哪些时段可行。考虑两维区间dp。记 \(f_{l,r}\) 代表时刻开区间 \((l,r)\) 内的所有进攻者都被消灭的最低成本。注意这里需要 \(l<a,b<r\),也就是完全包含。转移时必然要找到距离最远的一个,然后在某个 \(k\in [a_{max},b_{max}]\) 时刻以 \(d_{max}\) 的功率开放,那所有经过时刻 \(k\) 的进攻者都不需要考虑了,只需考虑 \((l,k),(k,r)\) 范围内的进攻者。于是得到 \(f_{l,r}=\min (f_{l,k}+f_{k,r}+d_{max})\)。这也是为啥使用开区间更方便。
Hack Protection「NEERC 2013」
枚举右端点,每一位独立考虑。那么 \(\&\) 和一定是先一段 \(0\) 然后一段 \(1\)。把分界线取出来,对每一位的分界线放一起排序,就可以知道每一段的 \(\&\) 的值。然后问题转化为求一段区间内等于某个数的数的个数。直接扫描线+哈希表即可。时空复杂度 \(O(n\log V)\),\(V\) 是值域。
[BZOJ4973]比特战争
首先不考虑边的限制(这也是我一开始理解的题意)。那么对于一个连通块,显然是找一个最便宜的地方买需求最多的地方的兵的数量。现在加上边的限制那么还需对瓶颈路,也就是最小生成树中最大的边的边权取 \(\max\)。由于这个边权可以特别大,我们还有另一种方法占领整个连通块——对于这条边的两侧分别占领。于是直接kruskal,每次合并时把答案与 \(\min(ans_u+ans_v,\max(w(u,v),\max a_x)*\min(b_x)),x\in S_u\cup S_v\) 取 \(\min\)。其中 \(S_u\) 代表 \(u\) 当前的连通块集合。时间复杂度 \(O(n\log n)\)。
事后检查「ICPC World Finals 2019」
暴搜+模拟,稍微剪枝即可通过。