Aug. 2023 普及组模拟赛 3
T1 最大生成树
Meaning
给定一个完全图,两点之间的边权为这两个点点权之差的绝对值,求这个图的最大生成树。
Solution
对于最小生成树,我们可以考虑 Kruskal 算法。
Kruskal 算法的正确性证明
这里所说的 Kruskal 算法用来求最小生成树。
假设 Kruskal 算法求得的生成树所包含的边被包含于边集 \(T\) 中,构成该图的最小生成树的的边被包含于边集 \(S\) 中。每次从 \(T\) 中选出边权最小且不包含在 \(S\) 的边放入 \(S\) 中,设该边为 \(e_1\),边权为 \(w_1\)。假设图 \(G\) 中包含且仅包含 \(S\) 中的边,则 \(e_1\) 的两个端点必定在 \(G\) 中相连。所以,\(e_1\) 的两个端点必定被一条包含他们的最近公共祖先的路径和 \(e_1\) 同时相连。换言之,此时 \(G\) 中存在且仅存在 \(1\) 个环,且包含 \(e_1\)。
接下来,从这个环中取出一条权值最小的边放入 \(T\) 中,设该边为 \(e_2\),边权为\(w_2\)。此时环被破坏,\(G\) 变为一棵树。因为若这个环之前就在 \(T\) 中时,为了使 \(T\) 中无环,\(e_1\) 不可能在 \(T\) 中,所以一定有不在 \(T\) 中的 \(e_2\)。重复上述操作,直到两边集边权和相等。
假设 \(w_1>w_2\),\(e_2\) 被 Kruskal 算法舍弃是因为 \(e_2\) 的两个端点已经有更优的连接方案,再次连接会形成回路,这与 \(S\) 作为最小生成树最优的特性矛盾,不成立。
假设 \(w_1<w_2\),由于 \(e_2\) 不是包含在 \(e_1\) 中,就是包含在操作前 \(G\) 中 \(e_1\) 两端点经过他们最近公共祖先的路径中,所以后者一定不如前者更优。故 \(S\) 中理应包含的是 \(e_1\) 而非 \(e_2\),且也能达到联通“环”中点的效果,与 \(S\) 的最优性矛盾,不成立。
综上所述,Kruskal 算法求出的 \(T\) 的边权和满足不经操作就与 \(S\) 的边权和相等,得证。
然而,这道题用 Kruskal 模板解决建图就需要 \(O(n^2)\),效率较低。我们可以直接利用 Kruskal 的贪心思想解题。
对于一个点权的集合 \(S\),设其中最大的点权为 \(S_p\),小的为 \(S_q\),则最长的一条边的长度为 \(S_p-S_q\)。而若要连接所有点,可得第 \(i\) 个点到所有点的路径中最长的为 \(\max_{S_i-S_q,S_p-S_i}\)。因为点 \(p\) 与 \(q\) 的连通性无需考虑,所以对于每个点,只需通过 \(i\) 到 \(p\) 或 \(i\) 到 \(q\) 中边权较大的一条边将点 \(i\) 纳入以 \(p\) 到 \(q\) 为基础的体系中,就得出了最大生成树。
代码
#include<bits/stdc++.h>
using namespace std;
long long n,a[200000],ans;
int main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
sort(a+1,a+n+1);
for(int i=2;i<n;++i) ans+=max(a[i]-a[1],a[n]-a[i]);
ans+=a[n]-a[1];
printf("%lld",ans);
return 0;
}
T2 红黑树
Meaning
给定一棵树,树上各个节点的初始权值均为 \(0\),先后将各点边权变成 \(1\),每次操作后求出既包含权值为 \(0\) 的点又包含权值为 \(1\) 的点的子树个数。
Solution
在线算法复杂度较高,考虑离线算法。
如何 Hack 在线做法
目前,笔者所知的最优在线算法是对于每次操作,修改被操作节点及其所有祖先。但如果出现一个长链,最劣单次修改复杂度会达到 \(O(n)\),总复杂度为 \(O(n^2)\),效率较低。
对于一棵树,设其中第一个被改变的节点在第 \(i\) 次操作被改变,最后一次被改变的节点在第 \(j\) 次操作被改变,那么它对答案产生影响的时间段为 \(i\) 到 \(j-1\)(在第 \(j\) 个时间节点该树所有节点的权值已经变为 \(1\))。记录各个节点被改变的时间,通过 dfs 将下一级子树的贡献区间传给上一级子树,以记录每一个子树的贡献区间。
代码
#include<bits/stdc++.h>
using namespace std;
struct node{
int end,next;
}edge[100100];
struct stru{
int head,father,dfn,out;
}dot[100100];
int n,cnt,dfncnt,ans[100100];
inline void add(int x,int y){
edge[++cnt].next=dot[x].head;
edge[cnt].end=y;
dot[x].head=cnt;
}
inline void dfs(int x){
for(int i=dot[x].head;i>0;i=edge[i].next){
int tem=edge[i].end;
dfs(tem);
dot[x].dfn=min(dot[x].dfn,dot[tem].dfn);
dot[x].out=max(dot[x].out,dot[tem].out);
}
++ans[dot[x].dfn];
--ans[dot[x].out];
}
int main(){
scanf("%d",&n);
for(int i=2;i<=n;++i){
scanf("%d",&dot[i].father);
add(dot[i].father,i);
}
for(int i=1;i<=n;++i){
int tem;
scanf("%d",&tem);
dot[tem].dfn=dot[tem].out=i;
}
dfs(1);
for(int i=1;i<=n;++i){
ans[i]+=ans[i-1];
printf("%d ",ans[i]);
}
return 0;
}
T3 校门外的树
Meaning
给定几棵树的初始高度和初始日生长速度,每天在树木生长结束后改变一个区间内树的生长速度或查询一个区间内树高度之和。
Solution
理想的情况是每棵树的生长速度不变,这样对于第 \(i\) 棵树,若它的高度为 \(h_i\),生长速度为 \(v_i\),则第 \(k\) 天它的高度为:
\]
而对于生长速度的改变,可以将初始高度除去修改前生长的高度与修改速度后用相同天数生长的高度,即当在第 \(j\) 天速度增加 \(vp_i\) 时,\(h_i\) 应减去:
\]
因为,第 \(k\)(\(k>j\),且第 \(j+1\) 天到第 \(k\) 天没有改变速度)天的高度是:
\]
多次变速同理,因为每一次变速相当于将从第一天开始至今每天的生长速度都变成了上一次修改后的速度,所以只需要按当前速度进行以上操作而不需要依次处理不同的速度。
综上所述,可用线段树维护每棵树的高度与生长速度,第 \(k\) 次修改速度时同时按上文修改区间内树的初始高度,查询时便可直接由 \(h_i+v_i\times{k}\) 求出区间高度和,即区间速度和与天数之积加上区间初始高度和。
代码
#include<bits/stdc++.h>
using namespace std;
struct node{
int left,right;
unsigned long long height,tagh,tagv,speed;
}tree[3000000];
int n,m,opt,lft,rgt;
unsigned long long adding;
inline void build(int x,int y,int z){
tree[x].left=y;
tree[x].right=z;
if(y==z){
scanf("%llu%llu",&tree[x].height,&tree[x].speed);
return;
}
int mid=(y+z)>>1;
build(x<<1,y,mid);
build(x<<1|1,mid+1,z);
tree[x].height=tree[x<<1].height+tree[x<<1|1].height;
tree[x].speed=tree[x<<1].speed+tree[x<<1|1].speed;
}
inline void pushdown(int x){
if(tree[x].tagh){
tree[x<<1].height+=(tree[x<<1].right-tree[x<<1].left+1)*tree[x].tagh;
tree[x<<1|1].height+=(tree[x<<1|1].right-tree[x<<1|1].left+1)*tree[x].tagh;
tree[x<<1].tagh+=tree[x].tagh;
tree[x<<1|1].tagh+=tree[x].tagh;
tree[x].tagh=0;
}
if(tree[x].tagv){
tree[x<<1].speed+=(tree[x<<1].right-tree[x<<1].left+1)*tree[x].tagv;
tree[x<<1|1].speed+=(tree[x<<1|1].right-tree[x<<1|1].left+1)*tree[x].tagv;
tree[x<<1].tagv+=tree[x].tagv;
tree[x<<1|1].tagv+=tree[x].tagv;
tree[x].tagv=0;
}
}
inline void modify(int x,int y,int z,unsigned long long adh,unsigned long long adv){
if(z<tree[x].left||tree[x].right<y) return;
if(y<=tree[x].left&&tree[x].right<=z){
tree[x].height+=(tree[x].right-tree[x].left+1)*adh;
tree[x].speed+=(tree[x].right-tree[x].left+1)*adv;
tree[x].tagh+=adh;
tree[x].tagv+=adv;
return;
}
pushdown(x);
modify(x<<1,y,z,adh,adv);
modify(x<<1|1,y,z,adh,adv);
tree[x].height=tree[x<<1].height+tree[x<<1|1].height;
tree[x].speed=tree[x<<1].speed+tree[x<<1|1].speed;
}
inline pair<unsigned long long,unsigned long long> query(int x,int y,int z){
if(z<tree[x].left||tree[x].right<y) return {0,0};
if(y<=tree[x].left&&tree[x].right<=z) return {tree[x].height,tree[x].speed};
pushdown(x);
pair<unsigned long long,unsigned long long> p1=query(x<<1,y,z),p2=query(x<<1|1,y,z);
return {p1.first+p2.first,p1.second+p2.second};
}
int main(){
scanf("%d%d",&n,&m);
build(1,1,n);
for(int i=1;i<=m;++i){
scanf("%d%d%d",&opt,&lft,&rgt);
if(opt==1){
scanf("%llu",&adding);
modify(1,lft,rgt,-adding*i,adding);
}else{
pair<unsigned long long,unsigned long long> tem=query(1,lft,rgt);
unsigned long long ans=tem.first+tem.second*i;
printf("%llu\n",ans);
}
}
return 0;
}
T4 种树
Meaning
在一个长度一定的点列上,规定两点间的距离为两点编号之差的绝对值。对于每次询问,给出两个黑点间的最小距离和不能涂黑的点的编号,求有多少种给这些点涂黑的方案(可以一个也不涂)。
Solution
定义:当一个点列中,若有 \(x\) 个点,黑点间的最小距离为 \(y\),则方案数为 \(f_{x,y}\)。
下面,考虑第 \(i\) 次询问:
对于整个点列,方案数为 \(f_{n,k_i}\)。
对于不能涂黑的点,需要减去包含该点的方案数。
对于 \(x_i\) 的左边,假设点 \(x_i\) 被涂黑,则最靠近 \(x_i\) 的点与 \(x_i\) 的距离至少是 \(k_i\),即该点的最大编号为 \(x_i-k_i\)。可得,在该点左侧的区间内的点的任意一种涂色方案都可以与涂黑 \(x_i\) 构成一种新的方案,总共可以构成的方案数为 \(f_{x_i-k_i,k_i}\)。
而对于 \(x_i\) 右侧的区间,距离 \(x_i\) 最近的点的最小编号为 \(x_i+k_i\),与上述同理,该区间内可与 \(x_i\) 构成的合法方案有 \(f_{n-(x_i+k_i)+1,k_i}\) 种。
根据乘法原理,每一种 \(x_i\) 左侧的方案都能与一种右侧的方案结合成一种包含 \(x_i\) 的合法涂色方案,共有 \(f_{x_i-k_i,k_i}\times{f_{n-(x_i+k_i)+1,k_i}}\) 种需要排除的方案。
综上所述,总的涂色方案为:
\]
对于求 \(f_{x,y}\),考虑以下两种方法。
数学方法
先看这个植树问题:
有 \(n\) 棵树种在一条笔直的路上,每两颗树间距离为 \(s\),假设树没有宽度,求两端的树之间的距离。
因为两端都有树,所以只有 \(n-1\) 个树之间的空隙,总长度为 \(s\times{(n-1)}\)。
对于 \(f_{x,y}\),当选出 \(i\) 个点涂黑时,与植树问题同理,应至少空出 \((i-1)\times{(y-1)}\) 个点,剩下的 \(x-(i-1)\times{(y-1)}\) 个位置可以任意分配给这 \(i\) 个点做可行放置位置,即方案数为:
\]
而只有当 \(0\leq{i}\leq{x-(i-1)\times{(y-1)}}\) 时,才存在有贡献的的 \(C_{x-(i-1)\times{(y-1)}}^{i}\)。
i&\leq{x-i\times{y}+y+i-1}\\
0&\leq{x-i\times{y}+y-1}\\
i\times{y}&\leq{x+y-1}\\
i&\leq{\frac{x+y-1}{y}}(y>0)
\end{aligned}
\]
因此,最终形式为:
\]
对于求组合数,直接预处理出各个数阶乘(即其阶乘对于模数的逆元)即可。
时间复杂度为 \(O\left(\frac{n}{k}q\right)\)。
如何 Hack 纯数学做法
当 \(k\) 值极小且 \(n\) 值极大时,最劣复杂度会达到 \(O(n^2)\)。
一个可行的 Hack 数据生成器:
#include<bits/stdc++.h>
using namespace std;
int main(){
srand(time(0));
freopen("data.txt","w",stdout);
int n=rand()%7+1,m=rand()%7+1;
int a=1e5;
cout<<a<<" "<<a<<endl;
for(int i=1;i<1e5;++i) cout<<a-i<<" "<<2<<endl;
return 0;
}
该生成器生成的数据无法用映射表优化卡过。
动态规划方法
考虑将所有 \(f_{x,y}\) 预处理出来,应对重复出现的 \(k\)。
当点 \(x\) 被涂黑时,它能继承 \(f_{x-k,y}\) 的方案,因为此时距离点 \(x\) 最近的点为了与点 \(x\) 保持距离为 \(k\) ,距离 \(x\) 最近的点的编号应至少为 \(x-k\)。
当 \(x\) 不被涂黑时,最优的方案是继承 \(f_{x-1,y}\),因为它一定不包含将 \(x\) 涂黑的方案,且经过累加后它是 \(f_{j,y}(j\in{[1,x-1]})\) 中最大的。
可得状态转移方程:
\]
为了防止 \(f_{x-k,y}\) 越界,可以将数组反向存储,即 \(f_{n-i+1,y}\) 表示原来的 \(f_{i,y}\)。
综上所述,最终的状态转移方程为:
\]
实现时注意倒序枚举。
时间复杂度为 \(O(n+q)\)。
如何 Hack 纯动态规划做法
当 \(k\) 值和 \(n\) 值同时极大时,算法效率低。
综合
根据上文所述,两种方法分别适合 \(k\) 值大和 \(k\) 值小的情况,可以按 \(k\) 值分类处理。
由于动态规划方法空间复杂度极大,所以应该使阈值尽量小。
代码
#include<bits/stdc++.h>
using namespace std;
const long long mdr=998244353;
long long n,m,empty,step,ans,tim[100010],timt[100010],invt[100010],inv[100010],sum[620][100010],tema,temb;
inline void exgcd(long long a,long long b,long long &x,long long &y){
if(!b){
x=1,y=0;
return;
}
exgcd(b,a%b,x,y);
long long tem=y;
y=x-a/b*y;
x=tem;
}
inline long long math(long long x,long long y){
long long rtr=1;//任何个数中取 0 个的方案数均为 1。
for(long long i=1;i<=(x+y-1)/y;++i){
long long tem=x-(i-1)*(y-1);
long long plr=((tim[tem]*inv[tem-i])%mdr*inv[i])%mdr;
rtr=(rtr+plr)%mdr;
}
return rtr;
}
inline void dp(int x){
for(int i=n;i;--i) sum[x][i]=(sum[x][i+1]+1+sum[x][i+x])%mdr;
}
inline long long query(int x,int y){
if(y<0||y>n) return 1;
return sum[x][y];
}
int main(){
scanf("%lld%lld",&n,&m);
tim[1]=tim[0]=1,timt[1]=1;
for(int i=2;i<=n;++i){
tim[i]=(tim[i-1]*i)%mdr;
timt[i]=(timt[i-1]*tim[i])%mdr;
}
long long waste;
exgcd(timt[n],mdr,invt[n],waste);
invt[n]=(invt[n]%mdr+mdr)%mdr;
for(int i=n-1;i;--i) invt[i]=(tim[i+1]*invt[i+1])%mdr;
inv[1]=inv[0]=1;
for(int i=2;i<=n;++i) inv[i]=(invt[i]*timt[i-1])%mdr;
for(int i=1;i<502;++i){
dp(i);
for(int j=1;j<=n;++j) ++sum[i][j];
}
while(m--){
scanf("%lld%lld",&empty,&step);
if(step>500){
ans=(math(n,step)-(math(empty-step,step)*math(n-empty-step+1,step))%mdr+mdr)%mdr;
printf("%lld\n",ans);
}else{
tema=n-(empty-step)+1,temb=empty+step;
ans=(sum[step][1]-((query(step,tema)*query(step,temb))%mdr)%mdr+mdr)%mdr;
printf("%lld\n",ans);
}
}
return 0;
}
Reference
Aug. 2023 普及组模拟赛 3的更多相关文章
- ZROI 普及组模拟赛02总结
ZROI 普及组模拟赛02总结 先放[网址][http://zhengruioi.com/contest/96] 可能是有一段时间没有打这种正式的比赛了,今天打的很奇怪... T1 模拟水题 既然是普 ...
- 2017.1.16【初中部 】普及组模拟赛C组总结
2017.1.16[初中部 ]普及组模拟赛C组 这次总结我赶时间,不写这么详细了. 话说这次比赛,我虽然翻了个大车,但一天之内AK,我感到很高兴 比赛 0+15+0+100=115 改题 AK 一.c ...
- NOIP2018普及组模拟赛
向老师给的模拟赛,还没普及组难... 题目在洛谷团队里. 第一试三道水题,我46分钟就打完了,然后就AK了. 第二试一看,除了第二题要思考一段时间之外,还是比较水的,但是我得了Rank倒1,115分. ...
- nowcoder(牛客网)普及组模拟赛第一场 解题报告
蒟蒻我可能考了一场假试 T1 绩点 这题没什么好说的,应该是只要会语言的就会做. T2 巨大的棋盘 一个模拟题吧qwq,但是要注意取模的时候先加上n或者m再取模,要不然会错的. #include< ...
- 【有奖】NOIP普及组模拟赛 个人邀请赛 乐多赛
题目描述 日本数学家角谷有一个猜想:任意一个自然数,经过以下过程,最终会得到1.现在请你打印出任意一个数使用角谷猜想转换为1需要几次. 演变方式: 1.如果这个数为奇数,则将它×3+1.如果这个数为偶 ...
- 52-2018 蓝桥杯省赛 B 组模拟赛(一)java
最近蒜头君喜欢上了U型数字,所谓U型数字,就是这个数字的每一位先严格单调递减,后严格单调递增.比如 212212 就是一个U型数字,但是 333333, 9898, 567567, 313133131 ...
- ZROI提高组模拟赛05总结
ZROI提高组模拟赛05总结 感觉是目前为止最简单的模拟赛了吧 但是依旧不尽人意... T1 有一半的人在30min前就A掉了 而我花了1h11min 就是一个简单的背包,我硬是转化了模型想了好久,生 ...
- NOIP2017提高组 模拟赛15(总结)
NOIP2017提高组 模拟赛15(总结) 第一题 讨厌整除的小明 [题目描述] 小明作为一个数学迷,总会出于数字的一些性质喜欢上某个数字,然而当他喜欢数字k的时候,却十分讨厌那些能够整除k而比k小的 ...
- NOIP2017提高组 模拟赛13(总结)
NOIP2017提高组 模拟赛13(总结) 第一题 函数 [题目描述] [输入格式] 三个整数. 1≤t<10^9+7,2≤l≤r≤5*10^6 [输出格式] 一个整数. [输出样例] 2 2 ...
- NOIP2017提高组模拟赛 10 (总结)
NOIP2017提高组模拟赛 10 (总结) 第一题 机密信息 FJ有个很奇怪的习惯,他把他所有的机密信息都存放在一个叫机密盘的磁盘分区里,然而这个机密盘中却没有一个文件,那他是怎么存放信息呢?聪明的 ...
随机推荐
- 【软件】解决奥林巴斯生物显微镜软件OlyVIA提示“不支持您使用的操作系统”安装中止的问题
[软件]解决奥林巴斯生物显微镜软件OlyVIA提示"不支持您使用的操作系统"安装中止的问题 零.问题 资源在文末 问题如下,从奥林巴斯生物显微镜软件官网下载地址:https://l ...
- [T.4] 团队项目:团队代码管理准备
团队的代码仓库地址 [GitHub - Meng-XuanYu/JayJay-TeamVersionControl: A public repo for BUAASE2025 course homew ...
- 【ESP32】两种模拟 USB 鼠标的方法
上一篇水文中,老周给大伙伴们扯了关于 idf 中添加自定义 Arduino 组件的方案.这次咱们做一下 USB 鼠标玩玩. 很遗憾的是,老周无能,在 Arduino-esp32 组件依赖 TinyUS ...
- IDEA强制注册登录版本号:IntelliJ IDEA 2021.2.2
建议采用 IntelliJ IDEA 2021.2.2 版本进行 Evaluate for free 试用 IntelliJ IDEA 2021.3.3 以前的版本可以不用注册登录idea账户, ...
- 【自用】restful api 常用状态码
GET(SELECT):从服务器取出资源(一项或多项). POST(CREATE):在服务器新建一个资源. PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源). PATCH(UPD ...
- SpringMVC的执行过程
环境准备 package org.example.springmvclearn; public record Greeting(long id, String content) { } package ...
- Git错误,Updates were rejected because the tip of your current branch is behind
问题:Updates were rejected because the tip of your current branch is behind 在push代码时,遇到这种问题Updates wer ...
- 1、 为什么软件开发周期总是预估的2~3倍? 2、什么是分而治之? 3、了解 WBS
1.为什么软件开发周期总是预估的2~3倍? 首先,软件开发中经常会有需求变更的情况,客户或者利益相关者可能会提出新的需求或者改变现有的需求,这就得调整计划,增加了开发时间.其次,开发人员的技术和经验也 ...
- Kotlin 实现类似 C# 的 Event 事件代码
在c#中,内置了对事件的设计模式,你可以简单的 += 来订阅一个事件. Kotlin 目前我没有发现内置的支持(如果你发现了,请留言告诉我 tansm),但Kotlin 非常方便的运算符重载,自己实现 ...
- 探秘Transformer系列之(31)--- Medusa
探秘Transformer系列之(31)--- Medusa 目录 探秘Transformer系列之(31)--- Medusa 0x00 概述 0x01 原理 1.1 动机 1.2 借鉴 1.3 思 ...