题面

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\) 天它的高度为:

\[h_i+v_i\times{k}
\]

而对于生长速度的改变,可以将初始高度除去修改前生长的高度与修改速度后用相同天数生长的高度,即当在第 \(j\) 天速度增加 \(vp_i\) 时,\(h_i\) 应减去:

\[j\times{vp_i}
\]

因为,第 \(k\)(\(k>j\),且第 \(j+1\) 天到第 \(k\) 天没有改变速度)天的高度是:

\[\begin{aligned}&h_i+j\times{v_i}+(k-j)\times{(v_i+vp_i)}\\=&h_i+k\times{(v_i+vp_i)}-j\times{vp_i}\\=&(h_i-j\times{vp_i})+k\times{(v_i+vp_i)}\\\end{aligned}
\]

多次变速同理,因为每一次变速相当于将从第一天开始至今每天的生长速度都变成了上一次修改后的速度,所以只需要按当前速度进行以上操作而不需要依次处理不同的速度。

综上所述,可用线段树维护每棵树的高度与生长速度,第 \(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_{n,k_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\) 个点做可行放置位置,即方案数为:

\[C_{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}\)。

\[\begin{aligned}i&\leq{x-(i-1)\times{(y-1)}}\\
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}
\]

因此,最终形式为:

\[f_{x,y}=\sum_{i=0}^{\frac{x+y-1}{y}}C_{x-(i-1)\times{(y-1)}}^{i}
\]

对于求组合数,直接预处理出各个数阶乘(即其阶乘对于模数的逆元)即可。

时间复杂度为 \(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,y}=f_{x-1,y}+1+f_{x-k,y}
\]

为了防止 \(f_{x-k,y}\) 越界,可以将数组反向存储,即 \(f_{n-i+1,y}\) 表示原来的 \(f_{i,y}\)。

综上所述,最终的状态转移方程为:

\[f_{x,y}=f_{x+1,y}+1+f_{x+k,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

Kruskal求得的生成树是最小生成树的证明

Aug. 2023 普及组模拟赛 3的更多相关文章

  1. ZROI 普及组模拟赛02总结

    ZROI 普及组模拟赛02总结 先放[网址][http://zhengruioi.com/contest/96] 可能是有一段时间没有打这种正式的比赛了,今天打的很奇怪... T1 模拟水题 既然是普 ...

  2. 2017.1.16【初中部 】普及组模拟赛C组总结

    2017.1.16[初中部 ]普及组模拟赛C组 这次总结我赶时间,不写这么详细了. 话说这次比赛,我虽然翻了个大车,但一天之内AK,我感到很高兴 比赛 0+15+0+100=115 改题 AK 一.c ...

  3. NOIP2018普及组模拟赛

    向老师给的模拟赛,还没普及组难... 题目在洛谷团队里. 第一试三道水题,我46分钟就打完了,然后就AK了. 第二试一看,除了第二题要思考一段时间之外,还是比较水的,但是我得了Rank倒1,115分. ...

  4. nowcoder(牛客网)普及组模拟赛第一场 解题报告

    蒟蒻我可能考了一场假试 T1 绩点 这题没什么好说的,应该是只要会语言的就会做. T2 巨大的棋盘 一个模拟题吧qwq,但是要注意取模的时候先加上n或者m再取模,要不然会错的. #include< ...

  5. 【有奖】NOIP普及组模拟赛 个人邀请赛 乐多赛

    题目描述 日本数学家角谷有一个猜想:任意一个自然数,经过以下过程,最终会得到1.现在请你打印出任意一个数使用角谷猜想转换为1需要几次. 演变方式: 1.如果这个数为奇数,则将它×3+1.如果这个数为偶 ...

  6. 52-2018 蓝桥杯省赛 B 组模拟赛(一)java

    最近蒜头君喜欢上了U型数字,所谓U型数字,就是这个数字的每一位先严格单调递减,后严格单调递增.比如 212212 就是一个U型数字,但是 333333, 9898, 567567, 313133131 ...

  7. ZROI提高组模拟赛05总结

    ZROI提高组模拟赛05总结 感觉是目前为止最简单的模拟赛了吧 但是依旧不尽人意... T1 有一半的人在30min前就A掉了 而我花了1h11min 就是一个简单的背包,我硬是转化了模型想了好久,生 ...

  8. NOIP2017提高组 模拟赛15(总结)

    NOIP2017提高组 模拟赛15(总结) 第一题 讨厌整除的小明 [题目描述] 小明作为一个数学迷,总会出于数字的一些性质喜欢上某个数字,然而当他喜欢数字k的时候,却十分讨厌那些能够整除k而比k小的 ...

  9. NOIP2017提高组 模拟赛13(总结)

    NOIP2017提高组 模拟赛13(总结) 第一题 函数 [题目描述] [输入格式] 三个整数. 1≤t<10^9+7,2≤l≤r≤5*10^6 [输出格式] 一个整数. [输出样例] 2 2 ...

  10. NOIP2017提高组模拟赛 10 (总结)

    NOIP2017提高组模拟赛 10 (总结) 第一题 机密信息 FJ有个很奇怪的习惯,他把他所有的机密信息都存放在一个叫机密盘的磁盘分区里,然而这个机密盘中却没有一个文件,那他是怎么存放信息呢?聪明的 ...

随机推荐

  1. 区块链特辑——solidity语言基础(四)

    Solidity语法基础学习 七.事件: 事件 Event ·日志(log),是用来快速索引并查询过往资料的手段. ·而solidity是透过"事件"在区块链上写下日志,使用者或由 ...

  2. JDK7-日历类--java进阶day07

    1.Calendar类 用于获取或者修改时间,之前学的Date类,获取和修改时间的方法已经过时 2.Calendar对象的创建 Calendar类里面有很多抽象方法,如果创建对象就要全部重写,所以不能 ...

  3. 【Docker】容器数据卷

    Docker容器数据卷 第一次听说这个名字,我一直以为是数据卷(juǎn),后来查看官方英文文档的"volume"这个单词的时候,我才反应过来,这是容器数据卷(juàn),书卷的卷 ...

  4. Sql Server数据库远程连接访问设置

    步骤一:设置sql server数据库 1.以新建一个新用户名test作为远程连接登录名.在本地登录sql server数据库,安全性->右键用户名 2.点击根目录右键,选择属性 选择安全性 选 ...

  5. 20241107,LeetCode 每日一题,使用 Go 计算两数相加

    思路 模拟加法:链表存储的是逆序数位,因此从头节点开始,逐位相加可以模拟正常的加法.每两个节点的值相加,并记录进位. 逐节点相加: 创建一个新的链表,用于存储结果,每次将两个链表对应节点的值加上进位值 ...

  6. 结合laravel深入理解php的服务容器和依赖注入

    原文:laravel 学习笔记 -- 神奇的服务容器 容器,字面上理解就是装东西的东西.常见的变量.对象属性等都可以算是容器.一个容器能够装什么,全部取决于你对该容器的定义.当然,有这样一种容器,它存 ...

  7. Vue(六)——条件渲染

    Vue--条件渲染 v-if.v-else-if.v-else v-if 指令用于条件性地渲染一块内容,表达式的值为 true --渲染. false--不渲染 v-if.v-else-if.v-el ...

  8. Sentinel——熔断规则

    目录 熔断规则 慢调用比例 慢比例调用代码实现 自定义异常处理器(返回响应流) 自定义异常处理类 测试 自定义异常处理器(返回页面) 异常处理器 定义页面 测试 熔断规则 现代微服务架构都是分布式的, ...

  9. windows oracle11gR2安装使用

    安装 plsql安装 https://blog.csdn.net/li66934791/article/details/83856225 配置tns # tnsnames.ora Network Co ...

  10. jsp技术之“如何在jsp中判断属性为空”

    一.判断对象列表为空不显示某段代码 <%-- 展开子属性 --%> <c:if test="${not empty product.variations}"> ...