【2024.09.15】NOIP2024 赛前集训(2)
A
最大的难点戏剧性地变成了二叉搜索树是什么。
先根据已知序列把二叉树建出来,忘了二叉搜索树的移步 二叉搜索树 & 平衡树 - OI Wiki (oi-wiki.org)
根据题意, 想到dp计数,\(f[u]\) 表示 \(u\) 子树内的答案, 则有转移:
\[f[u] = f[lson] \times f[rson] \times C_{siz[lson] + siz[rson]}^{siz[lson]} \]注意!对于只有一个儿子的那些点就老老实实写分类讨论,不要写成一坨!写一坨必错!!!
时间复杂度 \(O(TN)\)
/*think twice, code once
n, m, type
ci
ui,vi,li
q
ai,bi
*/
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
#define int ll
using namespace std;
using ll = long long;
const int N = 1e3 + 5;
const int mod = 1e9 + 7;
int n, T, rt;
int a[N], ch[N][2], siz[N], inv[N], fac[N], f[N];
int quickmod(int x,int y){
int res = 1;
while(y){
if(y & 1) res = res * x % mod;
x = (x * x) % mod;
y >>= 1;
} return res;
}
inline int C(int x, int y){
return fac[x] * inv[y] % mod * inv[x-y] % mod;
}
void insert(int nw, int u){
if(u < nw){
if(!ch[nw][0]) ch[nw][0] = u;
else insert(ch[nw][0],u);
}
else {
if(!ch[nw][1]) ch[nw][1] = u;
else insert(ch[nw][1],u);
}
}
void dfs(int u){
if(!u) return ;
f[u] = 1;
siz[u] = 1;
dfs(ch[u][0]);
dfs(ch[u][1]);
siz[u] += siz[ch[u][0]] + siz[ch[u][1]];
if(ch[u][0] && ch[u][1]) f[u] = f[ch[u][0]] * f[ch[u][1]] % mod * C(siz[ch[u][0]] + siz[ch[u][1]], siz[ch[u][0]]) % mod;
else if(ch[u][0]) f[u] = f[ch[u][0]];
else if(ch[u][1])f[u] = f[ch[u][1]];
}
signed main(){
freopen("bst.in","r",stdin);
freopen("bst.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
fac[0] = 1; F(i,1,1000) fac[i] = i * fac[i - 1] % mod;
inv[1000] = quickmod(fac[1000],mod-2);
G(i,999,1) inv[i] = inv[i + 1] * (i + 1) % mod;
cin >> T;
while(T --){
memset(ch, 0, sizeof(ch));
memset(siz, 0, sizeof(siz));
cin >> n;
F(i, 1, n) {
cin >> a[i];
if(i==1) rt = a[i];
else insert(rt, a[i]);
}
dfs(rt);
cout << (f[rt] + mod - 1) % mod << '\n';
}
return 0;
}
B
思维难度极高的一道小码量题目。
将 a 看作 1, b 看作 2, 那么 连续字母的替换 等效于 连续字母求模3意义下的和。
那么对于原串的任意合法区间(“合法”指存在相邻相同字符), 如果区间和模 3 意义下等于 1,则该区间最后可以通过一系列操作最后变为字符 a;等于 2 最后会变为字符 b;等于 0 则说明这段区间无法缩成一个字符(充分但不必要,例如aba)。
于是我们的任务便转化为:在原串上分段,然后把每段转化成一个字符,问求能得到多少种本质不同的字符串。
并且我们还能发现,我们实际上并不需要在划分的时候保证每个划分段内有相邻相同字符,因为 不合法 的划分可以转化为合法的划分!
注意:不合法仅指区间内不存在相邻相同字符,但是区间和模 3 仍不能为 0,不然无法缩成一个字符!
对序列求mod 3 意义下的前缀和 \(S\) ,然后用 dp 计数, \(f[i]\) 表示 \(1 \sim i\) 的所有划分方式, 得到的结果包括 1 和 2。记 \(val \in {0,1,2 }\)。 \(las[val]\) 表示上一个前缀和为 val 的位置, 有转移:
\[f[i] =\sum_{val =0 且 val \ne S[i]}^{2} f[las[val]] \]只有 \(S[i] - val \ne 0\) 的情况才能转移, 因为如果 \(= 0\) 代表 \(las[val]+1 \sim i\) 这个区间没办法合并成一个字母,即 不合法。
时间复杂度 \(O(TN)\)。
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
using namespace std;
using ll = long long;
const int N = 1e7 + 5;
const int mod = 1e9 + 7;
char s[N];
int a[N], las[N], f[N];
int T, n;
signed main(){
// freopen("alpha.in","r",stdin);
// freopen("alpha.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >>T;
while(T--){
memset(f, 0, sizeof(f));
memset(las, 0, sizeof(las));
cin >> (s + 1);
bool tag = 0;
n = strlen(s + 1);
F(i, 1, n) {
a[i] = (a[i-1] + s[i]-'a' + 1) % 3;
if(i > 1 && s[i] == s[i-1]) tag = 1;
}
if(!tag){
cout << 1 << '\n';
continue;
}
F(i, 1, n){
f[i] = (a[i]>=1);
F(j, 0, 2) if(j != a[i]) f[i] = (f[i] + f[las[j]]) % mod;
//如果last[j] + 1 ~ i 的区间和(即 a[i] - j) 为0, 那么就会被缩成 ab 或 ba , 是不合法的
//所以只有区间和在模意义下是1或者2才能缩成a或者b
las[a[i]] = i;
}
cout << f[n] << '\n';
}
return 0;
}
C
给定起点 \(a[i]\), 最大长度不超过 \(b[i]\), 也就是最小化最大长度,熟悉的话容易想到 Kruskal 重构树.
那么就先建树,再找到最远能向上走到的祖先节点的位置 \(u\),答案就是以 \(u\) 为根的子树内出现最多次数的颜色编号(颜色信息都储存在叶子上)
对于这些颜色信息的合并,可以使用启发式合并保证复杂度。
一些细节:
- 倍增往上跳的时候和求 \(lca\) 一样,不能跳到 0 位置去了。
- 注意一下启发式合并
swap
的具体手法,很有意思。 - 学习一下代码中的分类讨论,很清晰且不容易出错。
时间复杂度 \(O(NlogN)\)。
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
#define fi first
#define se second
using namespace std;
using ll = long long;
const int N = 5e5 + 105;
struct node1{
int u, v, w;
}e[N];
struct node2{ int ls, rs, w, num, col; }tr[N << 1];
unordered_map<int, int> cnt[N << 1];
int n, m, q, idx, type;
int c[N << 1], fa[N << 1], pa[N << 1][22];
inline int get(int x){
return (fa[x] != x) ? fa[x] = get(fa[x]) : fa[x];
}
void kruskal(){
sort(e + 1, e + m + 1, [&](node1 p, node1 q){return p.w < q.w;});
F(i, 1, n*2) fa[i] = i;
idx = n;
F(i, 1, m){
int fu = get(e[i].u), fv = get(e[i].v);
if(fu == fv) continue;
// cout << fu << ' ' << fv << '\n';
tr[++idx] = (node2){fu, fv, e[i].w};
fa[fu] = idx;
fa[fv] = idx;
}
}
void dfs(int u,int f){
pa[u][0] = f;
F(i,1,20) pa[u][i] = pa[pa[u][i-1]][i-1];
if(tr[u].ls){
int v = tr[u].ls;
dfs(v, u);
if(cnt[u].size() < cnt[v].size()) {
swap(cnt[u], cnt[v]);
//cnt 在 ask 的时候不会用到, 所以交换虽然不符合实际但是不影响答案
tr[u].num = tr[v].num;
tr[u].col = tr[v].col;
}
for(auto p : cnt[v]){
cnt[u][p.fi] += p.se;
if(tr[u].num < cnt[u][p.fi]){
tr[u].num = cnt[u][p.fi];
tr[u].col = p.fi;
}
else if(tr[u].num == cnt[u][p.fi]) tr[u].col = min(tr[u].col, p.fi);
}
cnt[v].clear();
}
if(tr[u].rs){
int v = tr[u].rs;
dfs(v, u);
if(cnt[u].size() < cnt[v].size()) {
swap(cnt[u], cnt[v]);
tr[u].num = tr[v].num;
tr[u].col = tr[v].col;
}
for(auto p : cnt[v]){
cnt[u][p.fi] += p.se;
if(tr[u].num < cnt[u][p.fi]){
tr[u].num = cnt[u][p.fi];
tr[u].col = p.fi;
}
else if(tr[u].num == cnt[u][p.fi]) tr[u].col = min(tr[u].col, p.fi);
}
cnt[v].clear();
}
if(!tr[u].ls && !tr[u].rs){
tr[u].num = 1;
tr[u].col = c[u];
cnt[u][c[u]] ++;
}
}
inline int ask(int x, int lim){
G(i,20,0) if(pa[x][i] != 0 && tr[pa[x][i]].w <= lim) x = pa[x][i];
return tr[x].col;
}
signed main(){
freopen("garden.in","r",stdin);
freopen("garden.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m >> type;
F(i, 1, n) cin >> c[i];
F(i, 1, m){
int u, v, w;
cin >> u >> v >> w;
e[i] = {u, v, w};
}
kruskal();
F(i, 1, idx) if(get(i) == i) dfs(i,0);
cin >> q;
int ans = 0;
F(i, 1, q){
int a, b;
cin >> a >> b;
if(type == 2) a ^= ans, b ^= ans;
cout << (ans = ask(a, b)) << '\n';
}
return 0;
}
D
考场上以为考的是最短路,但最后发现又是并查集。出现很多次类似的误判了……
对于能相互到达的区间,是可以相互合并的,于是我们先用一个 vector 把区间存起来,然后在线段树上合并。
注意!合并是根据给出的位置 \(pos\) 来把所有相关区间都合并掉。这也是为什么要先合并再插入新的区间。
这样的话就只剩下单向的区间且没有 被大区间完全包含的小区间了。最后可以直接按题目所给的定义判断。
有一些细节标注在代码里了,还是很受益的。
时间复杂度 \(O(NlogN)\)。
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
using ll = long long;
const int N = 1e6 + 5;
const int mod = 1e9 + 7;
int n, cnt = 0, num = 0;
int rk[N * 2], op[N], st[N], ed[N], fa[N * 2], lx[N], rx[N];
vector<int> cur[N];
inline int get(int x){
return (fa[x] == x) ? x : fa[x] = get(fa[x]);
}
void modify(int u, int l, int r, int pos){
for(auto x : cur[u]){
lx[num] = min(lx[num], lx[x]);
rx[num] = max(rx[num], rx[x]);
fa[get(x)] = num;
}
cur[u].clear();
if(l >= r) return ;
int mid = (l + r) >> 1;
if(pos <= mid) modify(u * 2, l, mid, pos);
else modify(u * 2 + 1, mid + 1, r, pos);
}
void addedge(int u, int l, int r, int x, int y){
if(x <= l && r <= y){
cur[u].emplace_back(num);
return ;
}
int mid = (l + r) >> 1;
if(x <= mid) addedge(u * 2, l, mid, x, y);
if(y > mid) addedge(u * 2 + 1, mid + 1, r, x, y);
}
void add(int l, int r){
modify(1, 1, cnt, l);
modify(1, 1, cnt, r);
if(lx[num] +1 < rx[num]) addedge(1, 1, cnt , lx[num] + 1, rx[num] - 1);
//极易出错!一定要先modify在addedge! 并且不能写成 :
//if(l + 1 < r) addedge(1, 1, cnt , l + 1, r - 1);
//因为在modify中有 "双向可达区间"之间 的合并
//所以为什么 lx[num] 要 +1 ?
}
signed main(){
freopen("interval.in","r",stdin);
freopen("interval.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
F(i, 1, n){
cin >> op[i] >> st[i] >> ed[i];
if(op[i] == 1) rk[++cnt] = st[i], rk[++cnt] = ed[i];
}
sort(rk + 1, rk + cnt + 1);
cnt = unique(rk + 1, rk + cnt + 1) - rk - 1;
F(i, 1, n){
if(op[i] == 1){
++num;
fa[num] = num;
st[i] = lower_bound(rk + 1, rk + cnt + 1, st[i]) - rk;
ed[i] = lower_bound(rk + 1, rk + cnt + 1, ed[i]) - rk;
lx[num] = st[i], rx[num] = ed[i];
add(st[i], ed[i]);
}
else{
int u = get(st[i]), v = get(ed[i]);//注意st, ed在两种op值下的不同含义
if(u == v) cout << "YES\n";
else {
if((lx[v] < lx[u] && rx[v] > lx[u]) || (lx[v] < rx[u] && rx[v] > rx[u])) cout << "YES\n";//读清楚定义! 边界不能取等!
else cout << "NO\n";
}
}
}
return 0;
}
总结:熟悉概念,能够识别,学会运用!
标签:ch,15,int,2024.09,cin,num,rk,NOIP2024,mod From: https://www.cnblogs.com/superl61/p/18425899