正睿 25 年联赛联合训练 Day 14
正睿 25 年联赛联合训练 Day 14
前言
主播主播,你的正解确实很强,但还是太吃操作了。有没有更加简单又强势的算法推荐一下呢?
有的兄弟,有的,这么强的算法当然是不止一个了,一共有三位,都是当前版本 T0.5 的强势算法。掌握一个就能拿到 \(100+\) 的分数,三个全部掌握的话就可以阿克本场比赛了。
得分
| T1 | T2 | T3 | 总分 | 排名 |
|---|---|---|---|---|
| \(53\) | \(60\) | \(53\) | \(166\) | \(3/16\) |
题解
改完题不笑的可以确诊玉玉症了。
T1 生成树
正解
我们考虑先把 \(b\) 边删掉,只保留 \(a\) 边。此时每一个连通块内只能走 \(a\) 边。然后我们只需要用一些 \(b\) 边把连通块连起来就行了。那么我们实际上可以通过跑最短路来解决,保证在同一连通块内不会走 \(b\) 边即可。
不过这样还不够,观察样例发现,如果我们重复到达同一个连通块两次是不符合条件的。我们没有什么好的方法去限制,那么就考虑状压一下。直接状压复杂度是 \(O(2^n\text{poly}(m))\) 的,过不去,不过进一步观察可以发现,对于大小 \(\le 3\) 的连通块,重复到达两次一定是不如直接走的,所以不需要管他们。那么只需要状压大小 \(\ge 4\) 的连通块即可,复杂度就降到了 \(O(m2^{\tfrac{n}{4}}\log n)\)。
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e3 + 5;
const int Inf = 2e9 + 5;
int n, m, a, b;
int head[Maxn], edgenum;
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w}; head[u] = edgenum;
edge[++edgenum] = {head[v], u, w}; head[v] = edgenum;
}
int id[Maxn], cnt;
int fa[Maxn], siz[Maxn];
void init() {for(int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;}
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}
void merge(int x, int y) {
x = find(x), y = find(y);
if(x == y) return ;
if(siz[x] > siz[y]) swap(x, y);
fa[x] = y, siz[y] += siz[x];
}
int vis[80][1 << 17], dis[80][1 << 17];
struct Node {
int w, x, s;
bool operator < (const Node &b) const {return w > b.w;}
};
priority_queue <Node> q;
void dijkstra() {
for(int i = 1; i <= n; i++) {
for(int j = 0; j < (1 << cnt); j++) dis[i][j] = Inf;
}
q.push({0, 1, 0});
dis[1][0] = 0;
while(!q.empty()) {
int x = q.top().x, s = q.top().s;
q.pop();
if(vis[x][s]) continue;
vis[x][s] = 1;
int P = find(x);
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to;
int Q = find(to), ns = s;
if((edge[i].w == b && P == Q) || (id[Q] && ((s >> (id[Q] - 1)) & 1))) continue;
if(edge[i].w == b && siz[P] >= 4) ns |= (1 << (id[P] - 1));
if(dis[to][ns] > dis[x][s] + edge[i].w) {
dis[to][ns] = dis[x][s] + edge[i].w;
if(!vis[to][ns]) q.push({dis[to][ns], to, ns});
}
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> a >> b;
init();
for(int i = 1; i <= m; i++) {
int u, v, w; cin >> u >> v >> w;
add(u, v, w);
if(w == a) merge(u, v);
}
for(int i = 1; i <= n; i++) {
if(fa[i] == i) if(siz[i] >= 4) id[i] = ++cnt;
}
dijkstra();
for(int i = 1; i <= n; i++) {
int ans = Inf;
for(int j = 0; j < (1 << cnt); j++) ans = min(ans, dis[i][j]);
cout << ans << ' ';
}
return 0;
}
歪解
我们考虑直接枚举每一个点最后的最短路是由 \(pa+qb\) 组成的,那么我们只需要找到一条链符合这个条件,然后再跑一遍 Kruskal 看答案与最小生成树是否相同即可。这个可以直接用 BFS 在 \(O(n^3 m)\) 的复杂度内求得。不过我们可能有很多满足长度是 \(pa+qb\) 的链,只取一条是没有正确性的。考虑随机化,每次随机取一条链,这样随机几次后就有很大概率找到答案。复杂度 \(O(n^3 m)\),爆踩标算。
T2 树
Fun Fact:本题交互库并没有判断给出的 \(x\) 是否在 \([1,n]\) 范围内,所以只要有解就输出 TAK 0 就可以通过。
正解
首先容易发现每个限制对应的合法 \(x\) 的范围是一个连通块,那么我们就是要找出这些连通块交集中的一个点。这里有一个结论:我们只需要判断所有连通块深度最大的根是否满足条件即可。
证明如下:设该点为 \(p\),如果它不满足条件,那么说明有连通块完全在 \(p\) 子树内或完全在 \(p\) 子树外。如果它完全在子树内,说明 \(p\) 不是最深的根,矛盾;否则说明有两个连通块没有交集,显然交集一定为 \(0\),即无解。
用倍增求一下 LCA 和 \(k\) 级祖先即可,复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 3e5 + 5;
const int Inf = 2e9;
int T;
int n, m;
int head[Maxn], edgenum;
struct node {
int nxt, to;
}edge[Maxn << 1];
void add(int u, int v) {
edge[++edgenum] = {head[u], v}; head[u] = edgenum;
edge[++edgenum] = {head[v], u}; head[v] = edgenum;
}
int fa[Maxn][20], dep[Maxn];
void dfs(int x, int fth) {
fa[x][0] = fth; dep[x] = dep[fth] + 1;
for(int i = 1; i <= 19; i++) fa[x][i] = fa[fa[x][i - 1]][i - 1];
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to;
if(to == fth) continue;
dfs(to, x);
}
}
int lca(int u, int v) {
if(dep[u] < dep[v]) swap(u, v);
for(int i = 19; i >= 0; i--) {
if(dep[fa[u][i]] >= dep[v]) u = fa[u][i];
}
if(u == v) return u;
for(int i = 19; i >= 0; i--) {
if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
}
return fa[u][0];
}
int dis(int u, int v) {return dep[u] + dep[v] - 2 * dep[lca(u, v)];}
int kthfa(int x, int k) {
for(int i = 19; i >= 0; i--) {
if((k >> i) & 1) x = fa[x][i];
}
return !x ? 1 : x;
}
struct Node {
int a, b, d;
}lim[Maxn];
void solve() {
edgenum = 0;
for(int i = 1; i <= n; i++) head[i] = 0;
cin >> n >> m;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
add(u, v);
}
dfs(1, 0);
int ans = 0;
for(int i = 1; i <= m; i++) {
int a, b, d; cin >> a >> b >> d;
if(ans == Inf) continue;
lim[i] = {a, b, d};
int p = lca(a, b), dis = (d - (dep[a] + dep[b] - 2 * dep[p])) / 2;
if(dis < 0) {ans = Inf; continue;}
p = kthfa(p, dis); ans = dep[p] > dep[ans] ? p : ans;
}
bool flg = 1;
if(ans == Inf) {
cout << "NIE\n"; return ;
}
for(int i = 1; i <= m; i++) {
if(dis(ans, lim[i].a) + dis(ans, lim[i].b) > lim[i].d) flg = 0;
}
if(flg) cout << "TAK " << ans << '\n';
else cout << "NIE\n";
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> T;
while(T--) solve();
return 0;
}
歪解
如果直接暴力判断的话复杂度显然是 \(O(nm)\) 的,考虑打乱枚举点和限制的顺序,这样可以直接拿到 \(90\) 分。
更牛的歪解
我们采取一个必要探路和贪心的措施。对于每一个限制,显然 \(d_i-dis(a_i,b_i)\) 越小限制越严格。那么对于每个点我们只取前几十个最严格的限制进行判断,先把不可能成为答案的点判掉。然后对于剩下的备选点进行随机取点,判断所有的条件,有限次数内如果有就直接输出,否则就认为无解。这样可以直接通过。
T3 种树
正解
考虑用 dp 来求解。我们以队伍为对象进行 dp,设 \(dp(i,j)\) 表示当前组建好所有大小 \(\ge i\) 的队伍,已经放好 \(j\) 个人的方案数。转移的时候枚举大小为 \(i\) 的队伍有几个,转移方程为:
其中 \(b_i\) 表示限制 \(\ge i\) 的人的数量。显然直接枚举的复杂度就是 \(O(n^2\log n)\) 的,可以通过。
歪解
上面说了以队伍为对象进行 dp,那么现在以人为对象也是可以的。首先对 \(a_i\) 从大到小排序,对于每一个队伍,在 \(a_i\) 最小的地方统计这个队伍的贡献。设 \(dp(i,j)\) 表示当前遍历到第 \(i\) 个人,已经放好 \(j\) 个人的方案数。转移方程为:
显然直接转移是 \(O(n^3)\) 的,无法通过。考虑后面的和式,发现它可以写成下面形式:
发现它可以分成两个部分,分别只与 \(j-k\) 和 \(k\) 有关。发现这是下标和固定的形式,恰好模数是 \(998244353\),考虑利用 NTT 优化转移过程,这样就可以做到 \(O(n^2\log n)\) 了。
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 4096 + 5;
const int Inf = 2e9;
const int Mod = 998244353;
const int Gen = 3;
const int InvG = 332748118;
int Add(int x, int y) {return x + y >= Mod ? x + y - Mod : x + y;}
void pls(int &x, int y) {x = Add(x, y);}
int Del(int x, int y) {return x - y < 0 ? x - y + Mod : x - y;}
void sub(int &x, int y) {x = Del(x, y);}
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) res = 1ll * res * a % Mod;
a = 1ll * a * a % Mod; b >>= 1;
}
return res;
}
int inv(int a) {return qpow(a, Mod - 2);}
int n, a[Maxn], r[Maxn], len = 1;
int f[Maxn], g[Maxn], dp[Maxn][Maxn];
void init() {
f[0] = 1; for(int i = 1; i <= n; i++) f[i] = 1ll * f[i - 1] * i % Mod;
g[n] = inv(f[n]); for(int i = n; i >= 1; i--) g[i - 1] = 1ll * g[i] * i % Mod;
}
void NTT(int *a, int len, int typ) {
for(int i = 0; i < len; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) * (len >> 1));
for(int i = 0; i < len; i++) if(i < r[i]) swap(a[i], a[r[i]]);
for(int h = 1; h < len; h <<= 1) {
int cur = qpow(typ == 1 ? Gen : InvG, (Mod - 1) / (h << 1));
for(int i = 0; i < len; i += (h << 1)) {
int w = 1;
for(int j = 0; j < h; j++, w = 1ll * w * cur % Mod) {
int x = a[i + j], y = 1ll * w * a[i + j + h] % Mod;
a[i + j] = Add(x, y);
a[i + j + h] = Del(x, y);
}
}
}
if(typ == -1) {
int iv = inv(len);
for(int i = 0; i < len; i++) a[i] = 1ll * a[i] * iv % Mod;
}
}
int F[Maxn], G[Maxn];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
init();
for(int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
dp[n + 1][0] = 1;
for(int i = n; i >= 1; i--) {
for(int j = 0; j < len; j++) F[j] = G[j] = 0;
for(int j = 0; j <= n - i; j++) F[j] = 1ll * dp[i + 1][j] * f[n - i - j] % Mod;
for(int j = 1; j <= min(a[i], n - i + 1); j++) G[j] = g[j - 1];
int m = n - i + 1; len = 1;
while(len <= (m << 1)) len <<= 1;
NTT(F, len, 1), NTT(G, len, 1);
for(int j = 0; j < len; j++) F[j] = 1ll * F[j] * G[j] % Mod;
NTT(F, len, -1);
for(int j = 0; j <= n - i + 1; j++) dp[i][j] = Add(1ll * F[j] * g[n - i - j + 1] % Mod, dp[i + 1][j]);
}
cout << dp[1][n] << '\n';
return 0;
}

浙公网安备 33010602011771号