【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;
}

总结:熟悉概念,能够识别,学会运用!

【2024.09.15】NOIP2024 赛前集训(2)的更多相关文章

  1. Lean Data Innovation Sharing Salon(2018.09.15)

    时间:2018.09.15地点:北京国华投资大厦

  2. 牛客网NOIP赛前集训营-提高组(第四场)游记

    牛客网NOIP赛前集训营-提高组(第四场)游记 动态点分治 题目大意: \(T(t\le10000)\)组询问,求\([l,r]\)中\(k(l,r,k<2^{63})\)的非负整数次幂的数的个 ...

  3. 牛客网NOIP赛前集训营-提高组(第四场)B区间

    牛客网NOIP赛前集训营-提高组(第四场)B区间 题目描述 给出一个序列$ a_1  \dots   a_n$. 定义一个区间 \([l,r]\) 是好的,当且仅当这个区间中存在一个 \(i\),使得 ...

  4. 牛客网NOIP赛前集训营-提高组(第四场)B题 区间

    牛客网NOIP赛前集训营-提高组(第四场) 题目描述 给出一个序列 a1, ..., an. 定义一个区间 [l,r] 是好的,当且仅当这个区间中存在一个 i,使得 ai 恰好等于 al, al+1, ...

  5. 牛客网NOIP赛前集训营-普及组(第二场)和 牛客网NOIP赛前集训营-提高组(第二场)解题报告

    目录 牛客网NOIP赛前集训营-普及组(第二场) A 你好诶加币 B 最后一次 C 选择颜色 D 合法括号序列 牛客网NOIP赛前集训营-提高组(第二场) A 方差 B 分糖果 C 集合划分 牛客网N ...

  6. 牛客网CSP-S提高组赛前集训营Round4

    牛客网CSP-S提高组赛前集训营 标签(空格分隔): 题解 算法 模拟赛 题目 描述 做法 \(BSOJ6377\) 求由\(n\)长度的数组复制\(k\)次的数组里每个连续子序列出现数字种类的和 对 ...

  7. NOIP赛前集训备忘录(含每日总结)(日更?。。。)

    NOIP赛前集训备忘录(含每日考试总结) 标签: 有用的东西~(≧▽≦)/~啦啦啦 阅读体验:https://zybuluo.com/Junlier/note/1279194 考试每日总结(这个东西是 ...

  8. 牛客CSP-S提高组赛前集训营1

    牛客CSP-S提高组赛前集训营1 比赛链接 官方题解 before:T1观察+结论题,T2树形Dp,可以换根或up&down,T3正解妙,转化为图上问题.题目质量不错,但数据太水了~. A-仓 ...

  9. 牛客网NOIP赛前集训营-提高组(第八场)

    染色 链接:https://ac.nowcoder.com/acm/contest/176/A来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 524288K,其他语言10 ...

  10. [NowCoder]牛客网NOIP赛前集训营-提高组(第七场)

    链接 A.中国式家长2 模拟题,毫无坑点 #include<bits/stdc++.h> #define REP(i,a,b) for(int i(a);i<=(b);++i) #d ...

随机推荐

  1. python增删查改实例

    本文介绍一个实例,即删除数据库中原有的表格TEST1,新建一个表格TEST2,并在TEST2中插入3行数据.插入数据以后,查询出ID=3的数据,读出,最后将其删除. 结果: 代码: ''' impor ...

  2. springcloud线上发布超时之feign(ribbon饥饿加载)

    springcloud线上发布超时系列文章: springcloud线上发布超时之feign(ribbon饥饿加载) springcloud线上发布超时之grpc springcloud线上发布超时方 ...

  3. 如何将png转为svg

    如何将png转为svg如图所示. 工具/原料 Inkscape 方法/步骤 1 打开Inkscape,"文件-打开"如图. 2  打开你需要转化的png图片.如图所示. 3 打开你 ...

  4. java_GUI2

    package GUi;import java.awt.*;public class GuI2 { public static void main(String[] args) { MyFrame n ...

  5. uni-app 小程序用户信息之头像昵称填写

    小程序获取用户头像昵称,微信又叒做妖,废除之前的接口,改成了头像昵称填写 通知:微信小程序端基础库2.27.1及以上版本,wx.getUserProfile 接口被收回,详见<小程序用户头像昵称 ...

  6. ES7学习笔记(四)字段类型(mapping)

    在上一节中,我们创建了索引,在创建索引的时候,我们指定了mapping属性,mapping属性中规定索引中有哪些字段,字段的类型是什么.在mapping中,我们可以定义如下内容: 类型为String的 ...

  7. rpm -Uvh *.rpm --nodeps --force

    rpm -Uvh *.rpm --nodeps --force 含义:-U:升级软件,若未软件尚未安装,则安装软件.-v:表示显示详细信息.-h:以"#"号显示安装进度.--for ...

  8. C++: 虚函数,一些可能被忽视的细节

    C++: 虚函数,一些可能被忽视的细节 引言:关于C++虚函数,对某些细节的理解不深入,可能导致我们的程序无法按预期结果运行,或是表明我们对其基本原理理解不够透彻.本文详细解答以下几个问题:实现多态, ...

  9. react 中获取子组件

    wrappedComponentRef={(form) => this.table = form}

  10. 聊聊 iframe, CSP, 安全, 跨域

    refer : https://www.cnblogs.com/kunmomo/p/12131818.html (跨域) https://segmentfault.com/a/119000000450 ...