2024.7.20 模拟赛总结
T1 lcd
Statement:
给定 \(n (1 \le n \le 10^8)\),问有多少对 \((i, j) (1 \le i, j \le n)\) 满足 \(\frac{xy}{\gcd(x, y)^2} \le 3\)。
Solution:
简单题。令 \(x' = \frac{x}{\gcd(x, y)}, y' = \frac{y}{\gcd(x, y)}\),枚举 \((x', y')\) 并计算即可。
T2 cut
Statement:
给定一颗有 \(n(1 \le n \le 5 \times 10^5)\) 个节点的树,其中有 \(n - 1\) 条边,其中第 \(i\) 条边连接 \((u_i, v_i)\)。问断掉第 \(i\) 条边后,\(u_i, v_i\) 所处非空连通子图个数分别为多少。答案对 \(998244353\) 取模。
Solution:
比较简单的换根 \(\rm DP\)。
先考虑如何求出整棵树的非空连通子图个数,\(\rm DP\) 容易解决。设 \(f_{u, 0 / 1}\) 是不选/选 \(u\) 时,以 \(u\) 为根子树的非空连通子图个数。显然 \(f_{u, 0} = \sum_{v \in subtree(u)} {f_{v, 0} + f_{v, 1}}\),\(f_{v, 1} = \prod_{v \in subtree(u)}{1 + f_{v, 1}}\)。
考虑切掉 \((u, v)\) 这条边后的影响,其中 \(u\) 是 \(v\) 的父亲,显然 \(v\) 中的答案是 \(f_{v, 1} + f_{v, 0}\)。现在计算 \(u\) 所处连通块的答案。显然我们需要割掉 \(v\) 对 \(u\) 带来的贡献,并加入 \(u\) 父亲割掉 \(u\) 后的贡献。于是考虑换根:设 \(g_{u, 0 / 1}\) 是不选/选 \(u\) 时,以 \(u\) 为根子树的非空连通子图个数。转移容易,但是有可能存在一个 \(f_{u, 0} + 1\) 为 \(mod\) 的倍数,不能使用逆元剔除贡献而是只能维护前缀和后缀积并将它们乘起来。
qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 10, mod = 998244353;
struct edge{
int v, id, next;
}edges[N << 1];
int head[N], idx;
int f[N][2], g[N][2];
struct Edge{
int u, v, ansu, ansv;
}ans[N];
vector<int> pre[N], suf[N];
int qpow(int x, int y){
int ret = 1; x %= mod;
while(y){
if(y & 1) ret = ret * x % mod;
x = x * x % mod; y >>= 1;
}
return ret;
}
void add_edge(int u, int v, int id){
edges[++idx] = {v, id, head[u]};
head[u] = idx;
}
int n;
void dfs1(int u, int fa){
f[u][1] = 1;
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v; if(v == fa) continue;
dfs1(v, u); f[u][0] = (f[u][0] + ((f[v][1] + f[v][0]) % mod)) % mod;
f[u][1] = f[u][1] * ((1 + f[v][1]) % mod) % mod; pre[u].push_back(1 + f[v][1]); suf[u].push_back(1 + f[v][1]);
}
for(int i = suf[u].size() - 2; i >= 0; i--) suf[u][i] = (suf[u][i + 1] * suf[u][i]) % mod;
for(int i = 1; i < suf[u].size(); i++) pre[u][i] = (pre[u][i - 1] * pre[u][i]) % mod;
suf[u].push_back(1);
// cout << u << " " << f[u][1] << " " << f[u][0] << "\n";
}
void dfs2(int u, int fa, int id){
int f0 = (g[fa][0] - ((f[u][0] + f[u][1]) % mod) + mod) % mod, f1 = id;
if(u != 1) g[u][0] = (f[u][0] + (f0 + f1)) % mod, g[u][1] = (f[u][1] * (1 + f1)) % mod;
else g[u][1] = f[u][1], g[u][0] = f[u][0], f1 = 0; int tot = 0;
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v; if(v == fa) continue;
int fu0 = (g[u][0] - ((f[v][0] + f[v][1]) % mod) + mod) % mod, fu1 = (((1 + f1) * (tot ? pre[u][tot - 1] : 1) % mod) * suf[u][tot + 1] % mod) % mod;
int ansv = (f[v][1] + f[v][0]) % mod, ansu = (fu0 + fu1) % mod;
// cout << u << " " << v << " " << f[u][0] << " " << f[u][1] << " " << f0 << " " << f1 << " " << fu0 << " " << fu1 << "\n";
if(u == ans[edges[i].id].u) ans[edges[i].id].ansu = ansu, ans[edges[i].id].ansv = ansv;
else ans[edges[i].id].ansu = ansv, ans[edges[i].id].ansv = ansu;
dfs2(v, u, fu1); tot++;
}
}
signed main(){
//freopen("3.in", "r", stdin);
//freopen("my.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
add_edge(x, y, i); add_edge(y, x, i);
ans[i] = {x, y, 0, 0};
}
dfs1(1, 0); dfs2(1, 0, 0);
for(int i = 1; i < n; i++) cout << ans[i].ansu << " " << ans[i].ansv << "\n";
//system("pause");
return 0;
}
/*
5
1 2
2 3
3 4
4 5
*/
T3 为了你唱下去
Statement:
给定一张有 \(n (1 \le n \le 10^5)\) 个点,有 \(m (1 \le 5 \times 10^5)\) 条带边权的边的无向图,其中 \(1 - c\) 是关键点。走过一条边会消耗该边边权大小的能量,如果当前能量小于边权则无法经过该点,但是到达关键点后又会将能量恢复到 \(r\)。现在问有多少非关键点可以从一个关键点出发,通过一条至少经过 \(k\) 个关键点的路径后到达。
\(30\)% 数据满足 \(n \le 500\)。
Solution:
图论好题。
首先容易发现两个关键点能够相互到达当且仅当两个点的最短距离小于等于 \(r\)。于是我们可以将关键点分为一些互不能到达且集合内部可以相互到达的集合。对于一个集合,我们可以先从任意点出发,走完里面的关键点后再去另外一个关键点。再由这些集合定义可知,只有集合大小大于等于 \(k\) 的集合才对答案有贡献。对于一个非关键点,若它与集合任一点的最短路径长度小于等于 \(r\),则该节点可以通过路径走到。\(\rm Floyd\) 可以拿到 \(30pts\) 的高分。
考虑优化合并集合的过程。我们首先将所有关键点的距离标记为 \(0\),跑一边 \(\rm Dijkstra\),求出所有非关键点距离最近的一个关键点。枚举每一条边,对于一条边 \(i\),若 \(w_i + dis[u] + dis[v] \le r\),合并 \(u, v\) 所在的集合,并查集容易做到 \(O(m \log n)\)。
接着我们将所有所在集合大小大于等于 \(k\) 的集合的关键点的 \(dis\) 设为 \(0\),再跑一遍 \(\rm Dijkstra\) ,计算 \(dis \le r\) 的点数即可。
qwq
#include<bits/stdc++.h>
#define int long long
#define pir pair<int, int>
using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10, INF = 1e18;
int n, m, c, r, k, fa[N], siz[N], dis[N], vis[N], from[N];
struct edge{
int v, w, next;
}edges[M << 1];
int head[N], idx;
void add_edge(int u, int v, int w){
edges[++idx] = {v, w, head[u]};
head[u] = idx;
}
int findfa(int x){return fa[x] = ((x == fa[x]) ? x : findfa(fa[x]));}
void merge(int x, int y){
int fx = findfa(x), fy = findfa(y);
if(fx != fy){fa[fx] = fy; siz[fy] += siz[fx];}
}
void buildc(){
for(int i = 1; i <= n; i++) dis[i] = INF;
priority_queue<pir, vector<pir>, greater<pir> >Q;
for(int i = 1; i <= c; i++) dis[i] = 0, Q.push({0, i}), from[i] = i;
while(!Q.empty()){
pir p = Q.top(); Q.pop();
int u = p.second, dist = p.first;
if(vis[u]) continue; vis[u] = true;
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v;
if((!vis[v]) && dis[v] > dist + edges[i].w && dist + edges[i].w <= r){
dis[v] = dist + edges[i].w; from[v] = from[u];
// cout << v << " " << u << " " << dist + edges[i].w << "\n";
Q.push({dis[v], v});
}
}
}
for(int u = 1; u <= n; u++){
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v;
if(dis[v] + edges[i].w + dis[u] <= r) merge(from[u], from[v]);
}
}
}
void getans(){
for(int i = 1; i <= n; i++) dis[i] = INF, vis[i] = false;
priority_queue<pir, vector<pir>, greater<pir> >Q;
for(int i = 1; i <= c; i++) if(siz[findfa(i)] >= k) dis[i] = 0, Q.push({0, i});
// for(int i = 1; i <= c; i++) cout << siz[findfa(i)] << "\n";
while(!Q.empty()){
pir p = Q.top(); Q.pop();
int u = p.second, dist = p.first;
if(vis[u]) continue; vis[u] = true;
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v; if(edges[i].w > r) continue;
if((!vis[v]) && dis[v] > dist + edges[i].w){
dis[v] = dist + edges[i].w;
Q.push({dis[v], v});
}
}
}
int ans = 0;
for(int i = c + 1; i <= n; i++) if(dis[i] <= r) ans++;
cout << ans << "\n";
for(int i = c + 1; i <= n; i++) if(dis[i] <= r) cout << i << " ";
}
signed main(){
//freopen("ex_sample8.in", "r", stdin);
//freopen("my.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> c >> r >> k;
for(int i = 1; i <= m; i++){
int x, y, w; cin >> x >> y >> w;
add_edge(x, y, w); add_edge(y, x, w);
}
for(int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
buildc(); getans();
return 0;
}
T4 入学
Statement:
略。
Solution:
DS 好题。
首先发现题目中的等价于是对于序列套序列的维护,这是困难的。考虑优化掉一维,注意到查询只是单点查询,因此我们考虑分别计算每个点的询问答案。于是先将询问和操作进行离线,并维护当前点 \(i\) 的队列信息。从前向后对 \(n\) 个队列扫描,考虑对队列 \(i\) 有贡献的操作,显然只有 \(L_j \le i \le R_j\) 的 \(j\) 才有贡献,套路的将操作拆成在 \(L_j\) 出现,\(R_j + 1\) 消失。
现在处理对于队列 \(i\) 的询问,考虑操作对答案的影响,设该询问的时间戳为 \(tim\),显然只有操作时间戳小于 \(tim\) 才可以生效。如果只有操作 \(1\),容易用一个线段树维护每个时间戳进行的操作,并在上面二分求出答案。具体的,我们维护一颗维护时间戳产生的操作 \([1, q]\) 的线段树。在从前到后扫描的过程中,对于一个还存在的操作,我们在它对应的时间戳上加上它添加的数量并记录加的数的种类。对于一个询问,我们查询一个位置 \(pos\) 满足 \(sum(1, pos) \ge cnt\)(其中 \(cnt\) 是查询的个数),在线段树上二分即可。
现在我们进而考虑操作二,操作二可能会在一些时间戳上清空整个序列,这是我们需要处理的。我们定义对于时间戳 \(tim\) 的前缀和数组 \(\rm Fsum_{tim}\) 为进行 \([1, tim]\) 包括删除操作的所有操作后的人数(可以为负数)。观察可以发现对于一个时间戳 \(tim\),\([1, tim]\) 中的 \(\rm Fsum\) 的最小值位置 \(P\) 即为上一次清空的位置。这可以用第二棵线段树维护,于是我们可以计算出清空前的人数,并用查询的 \(cnt\) 加上这些人数再在第一棵线段树上二分计算答案。
qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 10;
namespace Addtree{
#define ls (o << 1)
#define rs (o << 1 | 1)
#define mid ((l + r) >> 1)
int tsum[N << 2], lby[N << 2];
void pushup(int o){tsum[o] = tsum[ls] + tsum[rs];}
void add(int o, int l, int r, int pos, int k, int bel){
if(l == r){
tsum[o] += k; lby[o] = bel;
return;
}
if(pos <= mid) add(ls, l, mid, pos, k, bel);
else add(rs, mid + 1, r, pos, k, bel);
pushup(o);
}
int qrysum(int o, int l, int r, int R){
if(r <= R) return tsum[o];
if(R <= mid) return qrysum(ls, l, mid, R);
else return tsum[ls] + qrysum(rs, mid + 1, r, R);
}
int qrypos(int o, int l, int r, int val){
if(l == r) return lby[o];
if(tsum[ls] >= val) return qrypos(ls, l, mid, val);
else return qrypos(rs, mid + 1, r, val - tsum[ls]);
}
}
namespace Mintree{
int tmin[N << 2], tag[N << 2];
void pushup(int o){tmin[o] = min(tmin[ls], tmin[rs]);}
void addtagM(int o, int v){tmin[o] += v; tag[o] += v;}
void pushdown(int o){
if(!tag[o]) return;
addtagM(ls, tag[o]); addtagM(rs, tag[o]);
tag[o] = 0;
}
void add(int o, int l, int r, int pos, int k){
if(l >= pos){addtagM(o, k); return;}
pushdown(o);
if(pos <= mid) add(ls, l, mid, pos, k), addtagM(rs, k);
else add(rs, mid + 1, r, pos, k);
pushup(o);
}
int qrymin(int o, int l, int r, int R){
if(r <= R) return tmin[o];
pushdown(o);
if(R <= mid) return qrymin(ls, l, mid, R);
else return min(tmin[ls], qrymin(rs, mid + 1, r, R));
}
int qrySingle(int o, int l, int r, int pos){
if(l == r) return tmin[o];
pushdown(o);
if(pos <= mid) return qrySingle(ls, l, mid, pos);
else return qrySingle(rs, mid + 1, r, pos);
}
}
int n, m, q, tot, ans[N];
struct query{
int id, cnt, bel;
};
vector<query> Mo1[N], Mo2[N], qry[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> q;
for(int i = 1; i <= q; i++){
int opt, a, b, v1, v2; cin >> opt >> a >> b;
if(opt == 1){
cin >> v1 >> v2; Mo1[a].push_back({i, v2, v1});
Mo1[b + 1].push_back({i, -v2, v1});
}
if(opt == 2){
cin >> v1; Mo2[a].push_back({i, -v1, 0});
Mo2[b + 1].push_back({i, v1, 0});
}
if(opt == 3){
qry[a].push_back({i, b, ++tot});
}
}
for(int tim = 1; tim <= n; tim++){
for(int i = 0; i < Mo1[tim].size(); i++){
query Q = Mo1[tim][i];
Addtree::add(1, 1, q, Q.id, Q.cnt, Q.bel);
Mintree::add(1, 1, q, Q.id, Q.cnt);
}
for(int i = 0; i < Mo2[tim].size(); i++){
query Q = Mo2[tim][i];
Mintree::add(1, 1, q, Q.id, Q.cnt);
}
for(int i = 0; i < qry[tim].size(); i++){
query Q = qry[tim][i];
int sum = Addtree::qrysum(1, 1, q, Q.id), pre = min(0ll, Mintree::qrymin(1, 1, q, Q.id)), ths = Mintree::qrySingle(1, 1, q, Q.id);
if(Q.cnt <= ths - pre) ans[Q.bel] = Addtree::qrypos(1, 1, q, sum - (ths - pre) + Q.cnt);
}
}
for(int i = 1; i <= tot; i++) cout << ans[i] << "\n";
return 0;
}