『模拟赛题解』10.2 NOIP 模拟赛
10.2 模拟赛
T1. 万松园
Description
万松园地处繁华。其南临武广,东倚中山公园的得天独厚的位置,让生活在其中的人们习惯于四处游玩。然而 2019 年底疫情的到来,政府不得不采取封控的措施,以抵制疫情的蔓延。但这与万松园居民的习惯背道而驰,直接推行阻力太大。
疫情才刚刚开始,卫健委正考虑一种折中的措施:具体而言,万松园可以看作一颗树,树上有 \(n\) 个节点。调查显示,不同的路径有不同的“受欢迎程度”, 一个道路的受欢迎程度越小,其封控的成本越低。
由于封控就是让一个人与尽量少的其他人接触,只要将这棵树划分为一些较小的连通块就可以较为轻松地达到封控的目的。现在你要为卫健委写一个程序, 支持查询当封控所有“受欢迎程度”低于 \(K\) 的道路时,点 \(v\) 能到达的其他节点数量。
Solution
该题的核心是对询问离线处理。不难发现,能够保留的边数随着 \(K\) 的减小而增加,而且 \(K\) 较大能够保留的边在 \(K\) 较小时也能保留。如果将询问按照 \(K\) 的递减顺序排序,问题就变成了动态加边,询问图中某个点所在的连通块的大小。
这可以使用并查集很方便地解决。使用一个记录集合大小的并查集维护即可。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int n, q;
int f[maxn], sz[maxn];
int find(int x)
{
return (f[x] == x) ? f[x] : f[x] = find(f[x]);
}
void merge(int x, int y)
{
x = find(x);
y = find(y);
if (x == y)
return ;
if (sz[x] > sz[y])
swap(x, y);
f[x] = y;
sz[y] += sz[x];
}
struct Edge
{
int x, y, w;
} edge[maxn];
struct Query
{
int k, v, id;
} p[maxn];
struct Ans
{
int id, w;
} ans[maxn];
bool cmp(Edge u, Edge v)
{
return u.w > v.w;
}
bool cmp2(Query x, Query y)
{
return x.k > y.k;
}
bool cmp3(Ans x, Ans y)
{
return x.id < y.id;
}
int main()
{
freopen("lockdown.in", "r", stdin);
freopen("lockdown.out", "w", stdout);
cin >> n >> q;
for(int i = 1; i <= n; i ++)
{
f[i] = i;
sz[i] = 1;
}
for (int i = 1; i < n; i ++)
cin >> edge[i].x >> edge[i].y >> edge[i].w;
sort (edge + 1, edge + n, cmp);
for (int i = 1; i <= q; i ++)
{
cin >> p[i].k >> p[i].v;
p[i].id = i;
}
sort (p + 1, p + q + 1, cmp2);
int pos = 0, cnt = 0;
// cout << p[1].k << " :p[1] - k" << endl;
for (int i = 1; i <= q; i ++)
{
while (edge[pos + 1].w >= p[i].k && pos + 1 < n)
{
pos ++;
merge(edge[pos].x, edge[pos].y);
}
// cout << pos << " :pos\n";
if (pos == 0)
cout << 0 << endl;
else
{
ans[++ cnt].w = sz[find(p[i].v)] - 1;
ans[cnt].id = p[i].id;
}
}
sort (ans + 1, ans + cnt + 1, cmp3);
for (int i = 1; i <= cnt; i ++)
cout << ans[i].w << "\n";
return 0;
}
/*
4 1
3 1
1 2
*/
T2. 翻转有向图
Description
给出了一个 \(n\) 个点和 \(m\) 条边的有向图。顶点编号为 \(1 \sim n\)。图中没有重边和自环。
对于这 \(m\) 条边,求如果仅反转这一条边,是否会对整个图的强连通分量的数量产生影响?
反转边 \(i\) 的意思是一条点 \(x\) 向 \(y\) 的边,替换为从点 \(y\) 向 \(x\) 的边。
Solution
考虑翻转 \(u\) 到 \(v\) 这条边,会对强连通分量的数量产生什么样的影响?
- 原本 \(u\) 和 \(v\) 属于同一个强连通分量,翻转后不属于同一个强连通分量了,此时强连通分量的数量会 \(+ 1\)。这种情况要求当前边是从 \(u\) 到 \(v\) 的必经路径,即没有另一条不通过当前边的 \(u\) 到 \(v\) 的路径。同时存在从 \(v\) 到 \(u\) 的路径。
- 原本 \(u\) 和 \(v\) 属于同一个强连通分量,翻转后仍然属于同一个强连通分量,此时强连通分量的数量不变,这种情况要求当前边不是从 \(u\) 到 \(v\) 的必经路径,即存在另一条不通过当前边的 \(v\) 到 \(u\) 的路径。同时存在从 \(v\) 到 \(u\) 的路径。
- 原本 \(u\) 和 \(v\) 不属于同一个强连通分量,翻转后属于同一个强连通分量,此时强连通分量的数量 \(-1\), 这种情况要求原本不存在 \(v\) 到 \(u\) 的路径(加上翻转的边后存在了),并且存在另一条不通过当前边的 \(u\) 到 \(v\) 的路径。
- 原本 \(u\) 和 \(v\) 不属于同一个强连通分量,翻转后仍然不属于同一个强连通分量,此时强连通分量的数量不变,这种情况要求原本不存在 \(v\) 到 u$ 的路径(加上翻转的边后存在了),并且当前边是 \(u\) 到 \(v\) 的必经边。
然后我们发现这 种情况可以简化为两个验证条件:
- 是否存在 \(u\) 到 \(v\) 的路径。
- 当前边是否为必经的边。
对于这两个条件,如果直接做最坏情况下都是 \(O(m^2)\),考虑如何优化。
对于第一个条件直接 \(O(nm)\) 的搜索可以处理出所有点对之间的可达性。
对于第二个条件,我们利用无向图判环的染色法即可,同样是 \(O(nm)\) 的复杂度。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 5;
const int maxm = 2e5 + 5;
int n, m;
int flag[maxn];
vector <int> g[maxn];
struct Edge
{
int x, y;
} edge[maxm];
bool vis[maxn];
bool f1[maxn][maxn], f2[maxn][maxn];
void dfs1(int x, int rt)
{
f1[rt][x] = 1;
vis[x] = 1;
for (auto y : g[x])
{
if (!vis[y])
dfs1(y, rt);
}
}
void dfs2(int x, int k, bool f, int rt)
{
if (f)
{
if (flag[x] != k)
f2[rt][x] = 1;
}
else
flag[x] = k;
vis[x] = 1;
for (auto y : g[x])
{
if (!vis[y])
dfs2(y, k, f, rt);
}
}
int main()
{
freopen("turn.in", "r", stdin);
freopen("turn.out", "w", stdout);
cin >> n >> m;
for (int i = 1; i <= m; i ++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
edge[i].x = u;
edge[i].y = v;
}
for (int i = 1; i <= n; i ++)
{
memset(vis, 0, sizeof(vis));
dfs1(i, i);
}
for (int i = 1; i <= n; i ++)
{
memset(vis, 0, sizeof(vis));
memset(flag, 0, sizeof(flag));
vis[i] = 1;
for (int j = 0; j < g[i].size(); j ++)
{
int x = g[i][j];
if (!vis[x])
dfs2(x, j + 1, 0, i);
}
memset(vis, 0, sizeof(vis));
vis[i] = 1;
for (int j = g[i].size() - 1; j >= 0; j --)
{
int x = g[i][j];
if (!vis[x])
dfs2(x, j + 1, 1, i);
}
}
for (int i = 1; i <= m; i ++)
{
int u = edge[i].x, v = edge[i].y;
if (f1[v][u] != f2[u][v])
cout << "diff\n";
else
cout << "same\n";
}
return 0;
}
T3. k 进制
Description
你现在在学习二进制,但是你觉得二进制实在是太简单了。看着一道书本上的例题,你决定将它拓展到 \(k\) 进制。
现在给出一个长度为 \(n\) 的 \(k\) 进制数,可能含有前导 \(0\),你需要实现以下 \(4\) 种操作,共 \(m\) 次。
- \(1 \; x \; y\):将第 \(x\) 位上的数改为 \(y\);
- \(2 \; l \; r\):将第 \(l\) 位到第 \(r\) 位升序排列;
- \(3 \; l \; r\):将第 \(l\) 位到第 \(r\) 位降序排列;
- \(4 \; l \; r\):求第 \(l\) 位到第 \(r\) 位所组成的 \(k\) 进制数转为 \(10\) 进制数的结果,结果对 \(998244353\) 取模。
Solution
普通的做法考虑模拟,对于每个 2, 3 询问进行暴力排序,每次查询答案时从后往前扫一遍统计答案即可,时间复杂度 \(O(n^2 \log n)\)。
对于 \(k = 2\) 的情况,可以使用线段树进行区间推平并统计区间和,设区间 \([l, r]\) 的和为 \(sum\),先将 \([l, r]\) 全部赋值为 \(0\)。升序时将 \([l, \,l + sum - 1]\) 赋值为 \(1\),\([l + sum, \,r]\) 赋值为 \(0\),降序同理,可以在 \(O (n \log n)\) 的时间复杂度内完成。
正解是上述情况的拓展,设 \(t_{x,i}\) 表示在当前节点 \(x\) 统计的区间中,\(i\) 出现的次数,在升序的时候就分别修改 \([l, \, l + t_{x, 0} - 1]\),\([l + t_{x,0}, \, l + t_{x, 0} + t_{x, 1} - 1]\)。以此类推,降序同理。
设 \(s_x\) 表示节点所统计这段区间转成十进制的结果。在从儿子合并时 \(s_x = s_{x \ll 1} \times k^{r -mid} + s_{x \ll 1 | 1}\),在推平当前区间为 \(z\) 时 \(s_x = (\sum^{r-l}_{i=0} k^i) \times z\) ,其中 \(\sum^{r-l}_{i=0} k^i\) 和 \(k^{r -mid}\) 都可以事先预处理,所以最终单次操作的时间复杂度是 \(O(k \log n)\),最终复杂度 \(O (mk \log n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e4 + 5;
const int mod = 998244353;
#define int long long
int n, m, k, ans;
char s[maxn];
int sum[maxn], powk[maxn];
int tree[maxn * 4][10], lazytag[maxn * 4], sumtree[maxn * 4], g[10];
void update(int x, int l, int r)
{
int mid = (l + r) >> 1;
for (int i = 0; i < k; i ++)
tree[x][i] = tree[x << 1][i] + tree[x << 1 | 1][i];
sumtree[x] = ((sumtree[x << 1] * powk[r - mid] % mod) + sumtree[x << 1 | 1]) % mod;
}
void pushdown(int x, int l, int r)
{
if (lazytag[x] == -1)
return ;
int d = lazytag[x];
int mid = (l + r) >> 1;
for (int i = 0; i < k; i ++)
{
tree[x << 1][i] = 0;
tree[x << 1 | 1][i] = 0;
}
tree[x << 1][d] = mid - l + 1;
tree[x << 1 | 1][d] = r - mid;
lazytag[x << 1] = lazytag[x << 1 | 1] = d;
sumtree[x << 1] = sum[mid - l] * d % mod;
sumtree[x << 1 | 1] = sum[r - mid - 1] * d % mod;
lazytag[x] = -1;
}
void modify(int x, int l, int r, int L, int R, int d)
{
if (L > R) return;
if (l > R || r < L)
return ;
if (L <= l && r <= R)
{
sumtree[x] = sum[r - l] * d % mod;
for (int i = 0; i < k; i ++)
tree[x][i] = 0;
tree[x][d] = r - l + 1;
lazytag[x] = d;
return ;
}
pushdown(x, l, r);
int mid = (l + r) >> 1;
modify (x << 1, l, mid, L, R, d);
modify (x << 1 | 1, mid + 1, r, L, R, d);
update(x, l, r);
}
void query(int x, int l, int r, int L, int R, int d)
{
if (l > R || r < L)
return ;
if (L <= l && r <= R)
{
g[d] += tree[x][d];
return ;
}
pushdown(x, l, r);
// printf("%d :l %d :r %d :L %d :R\n", l, r, L, R);
int mid = (l + r) >> 1;
query(x << 1, l, mid, L, R, d);
query(x << 1 | 1, mid + 1, r, L, R, d);
}
void getans(int x, int l, int r, int L, int R)
{
if (l > R || r < L)
return ;
if (L <= l && r <= R)
{
ans = (ans + (sumtree[x] * powk[R - r] % mod)) % mod;
return ;
}
pushdown(x, l, r);
int mid = (l + r) >> 1;
getans(x << 1, l, mid, L, R);
getans(x << 1 | 1, mid + 1, r, L, R);
}
signed main()
{
freopen("ksystem.in", "r", stdin);
freopen("ksystem.out", "w", stdout);
cin >> n >> m >> k;
scanf("%s", s + 1);
powk[0] = sum[0] = 1;
for (int i = 1; i <= n; i ++)
{
powk[i] = powk[i - 1] * k % mod;
sum[i] = sum[i - 1] + powk[i];
}
for (int i = 1; i <= n; i ++)
modify(1, 1, n, i, i, (s[i] & 15));
while (m --)
{
int opt, l, r;
cin >> opt >> l >> r;
if (opt == 1)
modify(1, 1, n, l, l, r);
else if (opt == 2)
{
memset(g, 0, sizeof g);
for (int i = 0; i < k; i ++)
query(1, 1, n, l, r, i);
int L = l, R = 0;
for (int i = 0; i < k; i ++)
{
R = L + g[i] - 1;
modify(1, 1, n, L, R, i);
L = R + 1;
}
}
else if (opt == 3)
{
memset(g, 0, sizeof g);
for (int i = 0; i < k; i ++)
query(1, 1, n, l, r, i);
int L = l, R = 0;
for (int i = k - 1; i >= 0; i --)
{
// for (int j = 0; j < 10; j ++)
// cout << g[j] << " :g\n";
// printf("%lld :L %lld :R\n", L, R);
R = L + g[i] - 1;
// printf("%lld :L %lld :R\n", L, R);
modify(1, 1, n, L, R, i);
L = R + 1;
// printf("%lld :L %lld :R\n", L, R);
}
}
else
{
ans = 0;
getans(1, 1, n, l, r);
cout << ans << endl;
}
}
return 0;
}
T4. 游戏
略。