【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. [rCore学习笔记 024]多道程序与协作式调度

    写在前面 本随笔是非常菜的菜鸡写的.如有问题请及时提出. 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 本节重 ...

  2. 利用XtraBackup对MGR集群进行扩容

    运行了一段时间以后MGR集群,需要扩容节点,这是一个常见的需求.很多时候我们都喜欢用mysqldump工具来进行,因为这个工具有一个很好用的参数叫做master-data以及single-transa ...

  3. redis集群之哨兵模式

    redis集群之哨兵模式 1.集群部署 安装配置可参考一下地址: https://www.cnblogs.com/zhoujinyi/p/5569462.html 2.与springboot集成 这里 ...

  4. Camera | 10.linux驱动 led架构-基于rk3568

    前面文章我们简单给大家介绍了如何移植闪光灯芯片sgm3141,该驱动依赖了led子系统和v4l2子系统. V4L2可以参考前面camera系列文章,本文主要讲述led子系统. 一.LED子系统框架 L ...

  5. [rCore学习笔记 025]分时多任务系统与抢占式调度

    写在前面 本随笔是非常菜的菜鸡写的.如有问题请及时提出. 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 本节重 ...

  6. 全网最适合入门的面向对象编程教程:37 Python常用复合数据类型-列表和列表推导式

    全网最适合入门的面向对象编程教程:37 Python 常用复合数据类型-列表和列表推导式 摘要: 在 Python 中,列表是一个非常灵活且常用的复合数据类型.它允许存储多个项,这些项可以是任意的数据 ...

  7. Adobe Photoshop cc2018 Mac中文破解版下载

    下载地址在文章最末,下载之前,先看下安装教程. 前面有说过,2015年以前的老Mac电脑可以安装PS2018的版本,Adobe Photoshop cc2018最低系统需求:10.13以上就可以了,但 ...

  8. 删除链表倒数第N个节点(19)

    双指针法 双指针法主要是最开始有两个指针fast,slow都指向链表的虚拟头节点dummy,然后快指针先移动,这里需要先向后移动n+1位(因为你最终是要找到目标节点的前一个节点),然后slow和fas ...

  9. 【Azure Policy】使用deployIfNotExists 把 Azure Activity logs 导出保存在Storage Account

    问题描述 使用Azure Policy,对订阅下的全部Activity Log配置Diagnostic Setting,要求: 在Subscription或Management Group级别,针对未 ...

  10. springCloud allibaba 微服务引言

    微服务篇: springcloud 常见组件有哪些 nacos 的服务注册表结构是怎样的 nacos 如何支撑阿里内部数十万服务注册压力 nacos 如何避免并发读写冲突问题 nacos 和eurek ...