COCI 2021-2022 #4
COCI 2021-2022 #4 题解
T1
简单贪心。
T2
设 \(f_{k,u,v}\) 表示从 \(u\) 走至多 \(k\) 条边到达 \(v\) 的最短路。
可以直接 Floyd \(\mathcal O(n^4)\) 也可以上一个矩阵快速幂优化变成 \(\mathcal O(n^3\log n)\)。
Floyd 版:
#include <bits/stdc++.h>
using namespace std;
const int N = 75, inf = 0x3f3f3f3f;
int n, m, k, Q;
int w[N][N], f[N][N], g[N][N];
int main() {
scanf("%d%d", &n, &m);
memset(w, 0x3f, sizeof w);
for (int i = 1; i <= n; ++i) w[i][i] = 0;
for (int i = 1, x, y, z; i <= m; ++i) scanf("%d%d%d", &x, &y, &z), w[x][y] = min(w[x][y], z);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
f[i][j] = w[i][j];
scanf("%d%d", &k, &Q);
k = min(k, n - 1);
for (int _ = 1; _ < k; ++_) {
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[i][j] = f[i][j];
for (int K = 1; K <= n; ++K)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[i][j] = min(g[i][j], f[i][K] + w[K][j]);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
f[i][j] = g[i][j];
}
while (Q--) {
int u, v; scanf("%d%d", &u, &v);
if (f[u][v] == inf) printf("%d\n", -1);
else printf("%d\n", f[u][v]);
}
return 0;
}
矩乘优化版:
#include <bits/stdc++.h>
using namespace std;
const int N = 75, inf = 0x3f3f3f3f;
int n, m, k, Q;
struct mat {
int a[N][N];
mat() = default;
mat(int n) {
memset(a, 0x3f, sizeof a);
for (int i = 1; i <= n; ++i) a[i][i] = 0;
}
mat operator * (const mat &x) const {
mat res(n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= n; ++k)
res.a[i][j] = min(res.a[i][j], a[i][k] + x.a[k][j]);
return res;
}
mat operator ^ (int y) const {
mat x = *this, res(n);
while (y) {
if (y & 1) res = res * x;
x = x * x;
y >>= 1;
}
return res;
}
} f;
int main() {
scanf("%d%d", &n, &m);
f = mat(n);
for (int i = 1, x, y, z; i <= m; ++i) scanf("%d%d%d", &x, &y, &z), f.a[x][y] = min(f.a[x][y], z);
scanf("%d%d", &k, &Q);
k = min(k, n - 1);
f = f ^ k;
while (Q--) {
int u, v; scanf("%d%d", &u, &v);
if (f.a[u][v] == inf) printf("%d\n", -1);
else printf("%d\n", f.a[u][v]);
}
return 0;
}
T3
考虑枚举每种数,计算他的贡献。
首先将 \(a\) 离散化。
假设当前在计算 \(x\) 的贡献,记 \(S_i\) 为前 \(i\) 个数中 \(x\) 的出现次数,则区间 \([l+1,r]\) 合法当且仅当 \(S_r-S_l\gt r-l-(S_r-S_l)\),移项得 \(2(S_r-S_l)\gt r-l\),再整合一下变成 \(2S_r-r\gt 2S_l-l\)。
下文记 \(B_i=2S_i-i\)。
这样问题转化成对于每个 \(r\),\(0\sim r-1\) 中有多少个 \(l\) 使得 \(B_l\lt B_r\),如果直接用树状数组维护的话时间复杂度为 \(\mathcal O(n^2\log n)\)。
不妨来看看 \(B_i\) 的变化情况。举个例子,如下图:
可以发现如果有 \(m\) 个 \(x\),那么 \(B_i\) 会被分成 \(m+1\) 个区间,每个区间都是一个公差为 \(-1\) 的等差序列。
如果我们能用某种方法把每段里的数同时处理,那么总共需要处理的段数就是 \(\mathcal O(n)\) 了。
首先段内显然不会有贡献,因为段内的数是递减的。
只要依次从前往后考虑每个段就好了。
假设这个等差数列为 \(y,y-1,\cdots,x+1,x\),设 \(C_i\) 为 \(i\) 的出现次数,那么处理完这个段后需要将区间 \([x,y]\) 的 \(C\) 都加一。
设 \(D\) 是 \(C\) 的前缀和,那么对于每个 \(B_i\),它的贡献为 \(D_{B_i-1}\),那么对于这个等差数列中的数的总贡献就是 \(\sum\limits_{i=x-1}^{y-1}D_i\)。
设 \(E\) 是 \(D\) 的前缀和,那么总贡献变成了 \(E_{y-1}-E_{x-2}\)。
至此问题就变成了一个区间修改,求二阶前缀和的问题。
考虑通过差分将其变成单点修改,求三阶前缀和的问题。
不妨看看 ABC256F。
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 200005;
int n;
int a[N], _[N], tot;
vector <int> pos[N];
ll c[N*2][3];
void add(int x, ll y, int n) { for (int i = x; i <= n; i += i & -i) c[i][0] += y, c[i][1] += y * x, c[i][2] += y * x * x; }
ll query(int x) { ll res = 0; for (int i = x; i; i -= i & -i) res += c[i][0] * (x + 1) * (x + 2) - c[i][1] * (2 * x + 3) + c[i][2]; return res / 2; }
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), _[++tot] = a[i];
sort(_ + 1, _ + tot + 1), tot = unique(_ + 1, _ + tot + 1) - (_ + 1);
for (int i = 1; i <= n; ++i) a[i] = lower_bound(_ + 1, _ + tot + 1, a[i]) - _, pos[a[i]].push_back(i);
const int del = n + 1;
ll ans = 0;
for (int i = 1; i <= tot; ++i) {
pos[i].push_back(n + 1);
int lst = 0;
for (int j = 0; j < pos[i].size(); ++j) {
int y = 2 * j - lst + del, x = 2 * j - (pos[i][j] - 1) + del;
ans += query(y - 1) - (x > 2 ? query(x - 2) : 0);
add(x, 1, n + del), add(y + 1, -1, n + del);
lst = pos[i][j];
}
lst = 0;
for (int j = 0; j < pos[i].size(); ++j) {
int y = 2 * j - lst + del, x = 2 * j - (pos[i][j] - 1) + del;
add(x, -1, n + del), add(y + 1, 1, n + del);
lst = pos[i][j];
}
}
printf("%lld", ans);
return 0;
}
T4:
看到最大值最小,考虑二分答案,将问题转变成判断可行性。
想到一个贪心的思路,当以 \(i\) 为根的子树的策略确定时,如果 \(i\) 是黑色点,把 \(i\) 移到它的父亲依旧合法的话,那就令它的父亲变成黑色点。
为了方便我们称未找到对应黑色点的白色点为不合法点。
我们在 DFS 的过程中记录两个值:
- 距离最近的黑色节点
- 距离最远的不合法点
根节点需要特殊处理。
时间复杂度为 \(\mathcal O(n\log n)\)。
具体细节看代码。
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 200005;
const ll inf = 0x3f3f3f3f3f3f3f3f;
int n, k;
int head[N], ver[N*2], wei[N*2], nxt[N*2], cnt = 1;
ll l, r, lim;
int ans[N], tot; bool vis[N];
ll f[N][2];
void add(int u, int v, int w) {
ver[++cnt] = v, wei[cnt] = w, nxt[cnt] = head[u], head[u] = cnt;
}
void dfs(int u, int pre) {
ll mn = inf; //到最近黑点的距离
for (int i = head[u]; i; i = nxt[i]) {
int v = ver[i], w = wei[i];
if (i == pre) continue;
dfs(v, i ^ 1);
mn = min(mn, f[v][1] + w);
}
ll mx = -inf; //到最远不合法点的距离
if (mn > lim) mx = 0; //当前点是不合法点
for (int i = head[u]; i; i = nxt[i]) {
int v = ver[i], w = wei[i];
if (i == pre) continue;
if (f[v][0] + w + mn > lim)
mx = max(mx, f[v][0] + w);
}
if (!pre && mx >= 0 || mx + wei[pre] > lim) {
//当前点是根节点且还有不合法点或当前点不成为黑色点的话子树就不合法
//那么当前点就应该变成黑色点
ans[++tot] = u;
f[u][0] = -inf, f[u][1] = 0;
}
else f[u][0] = mx, f[u][1] = mn;
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1, u, v, w; i < n; ++i) scanf("%d%d%d", &u, &v, &w), add(u, v, w), add(v, u, w), r += w;
while (l < r) {
lim = l + r >> 1, tot = 0;
dfs(1, 0);
if (tot <= k) r = lim;
else l = lim + 1;
}
printf("%lld\n", l);
lim = l, tot = 0;
dfs(1, 0);
for (int i = 1; i <= tot; ++i) vis[ans[i]] = 1;
for (int i = 1; i <= n && tot < k; ++i) if (!vis[i]) ans[++tot] = i;
for (int i = 1; i <= tot; ++i) printf("%d ", ans[i]);
return 0;
}
T5:
非常套路的容斥,但是不知道一开始的做法为什么挂了。。
考虑合法的方案数等于总方案数减去不合法方案数
因为发现 \(n\le60,m\le15\),非常小,所以暴力枚举哪些路径钦定不合法,然后用并查集维护即可。
预处理 \(k\) 的幂次,时间复杂度为 \(\mathcal O(nm2^m\log n)\)。
具体细节看代码。
Code:
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
typedef long long ll;
const int N = 65, M = 20, mod = 1e9 + 7;
int n, m, k;
int mul[N];
int ans;
int path[M][N], len[M];
void add(int &a, int b) {
a += b;
if (a >= mod) a -= mod;
if (a < 0) a += mod;
}
namespace Graph {
int head[N], ver[N*2], nxt[N*2], cnt;
int id[N*2], idc;
void addedge(int u, int v, int Id) {
ver[++cnt] = v, id[cnt] = Id, nxt[cnt] = head[u], head[u] = cnt;
}
bool dfs(int u, int fa, int Id, int ed) {
if (u == ed) return true;
for (int i = head[u]; i; i = nxt[i]) {
int v = ver[i];
if (v == fa) continue;
if (dfs(v, u, Id, ed)) {
path[Id][++len[Id]] = id[i];
return true;
}
}
return false;
}
}
namespace DSU {
int fa[M];
void reset() { for (int i = 1; i < n; ++i) fa[i] = i; }
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) { x = find(x), y = find(y); if (x == y) return; fa[x] = y; }
}
int main() {
scanf("%d%d%d", &n, &m, &k);
mul[0] = 1; for (int i = 1; i < n; ++i) mul[i] = 1ll * mul[i - 1] * k % mod;
for (int i = 1, u, v; i < n; ++i) scanf("%d%d", &u, &v), ++Graph::idc, Graph::addedge(u, v, Graph::idc), Graph::addedge(v, u, Graph::idc);
ans = mul[n - 1];
for (int i = 0, u, v; i < m; ++i) scanf("%d%d", &u, &v), Graph::dfs(u, 0, i, v);
for (int S = 1; S < (1 << m); ++S) {
int tot = 0;
DSU::reset();
for (int i = 0; i < m; ++i) if (S >> i & 1) {
for (int k = 2; k <= len[i]; ++k)
DSU::merge(path[i][1], path[i][k]);
}
for (int i = 1; i < n; ++i) if (DSU::fa[i] == i) ++tot;
if (__builtin_popcount(S) & 1) add(ans, -mul[tot]);
else add(ans, mul[tot]);
}
printf("%d", ans);
return 0;
}