NOIP 模拟赛 1~10

CSP考完打算写题解了
写题解有啥用呢,大抵是总结吧。。
总不能让博客一直没东西
第一场走丢了(确信)

10.25 模拟2 mp场

100+70+0+0=170 pts rk13

T3暴力 INT_MAX,给我输出了mp的题解密码(蚌),T4暴力没时间测就交了。
T1挺能签的,大部分时间花在 T2 上,T3T4 没咋想
T3 考场上想的DP但是不会转移。

T1 集训

首先按权值从小到大排序,然后我们一定是让平均数尽可能小,所以一定是选一个前缀,但全选不一定最优,所以从前往后枚举集合的右端点,每次更新平均数,然后可以维护单指针表示权值第一个大于平均数的下标,因为平均数单调不降,所以指针是单调的,除排序复杂度 Θ(n),也可以每次二分找。

T2 害怕

考场写了个 树剖+线段树优化建图+两次拓扑排序+优先队列,怒写200+行,复杂度 Θ(nlog2n) 满,被卡70pts。

正解思路很简单,按照编号从小到大枚举每一条边,对于树边(蓝边)直接赋当前时间戳,对于非树边(白边)先将链上没有访问过的边按编号从小到大赋时间戳,再给该边赋时间戳,实现的时候使用并查集维护已经访问过的边,直接跳并查集根的父亲,这样总共遍历的复杂度是 Θ(n) 的,因为需要排序所以总复杂度还要带个 log

T3 负责

首先断开连通块就一定会出现一个位置左右两边都不包含,否则就会有交,考虑枚举这个位置(断点)。

先将区间按右端点排序,我们设 dpi 表示只考虑前 i 个区间,以 ri 为一个断点符合要求的最大贡献,转移考虑枚举之后的一个断点 j,即所有区间都不包括 j,计算从 ij 的区间选出前 k 大的贡献,使用优先队列维护 size,时间复杂度 Θ(n2logn)

T4 分神

10.26 模拟3 A层联测18

100+80+30=210 pts rk8

T1就想了半天,不知道埃氏筛复杂度,看着 1e7 跑着差不多就交了。

T2出博弈论,以为不可做,后来想到了直径端点是必胜的,就可以反着推回去了,没猜出来性质,打了个 n3但是数据太水了

T3是 xuany 的T2,好像还比那个简单点,考场上没看出性质,打最低档暴力和特殊性质。

T1 新的阶乘

分别考虑每一个素数,对该素数能造成贡献的只有它的倍数,直接枚举每个素数的倍数,再枚举该倍数能整除它的次数,对于第 x 个数,其次数为 n+1x,累加答案即可,复杂度 o(nloglognlogn)

T2 博弈树

60(80)pts:

博弈论,能转移到一个必败状态就是必胜状态,转移到的全是必胜状态才是必败状态,记住这句话就行。

dpx,i 表示当前棋子在 x 点,下一步要走的距离需要 >i 是必胜状态还是必败状态,我们设直径长度为 mx,则 dpx,mx=0,解释为不可能有长度大于 mx 的路径,必败,对于每个询问 x,即询问 dpx,0,考虑从后往前转移,转移时枚举每一个符合要求的点,lca 求距离要再套个 log,复杂度 Θ(n3logn)

小优化:提前预处理每个点对间的距离,复杂度 Θ(n2+n3)

正解:

首先直径端点一定是必胜状态,那考虑删除直径端点得到一个新树(命名为 t2),那么 t2 的直径端点也是必胜状态,因为先手可以先把棋子移动到 t2 另一个端点,然后后手就会移动到原树的一个端点,且后手移动距离一定大于先手移动距离所以合法。这样一直删的话最后只会剩下一个(直径长度为奇数)或两个点(直径长度为偶数),前者该点先手必败,因为它不能自己跳自己,只能让给后手,后者都是先手必胜,所以只需要判断直径长度,若为奇数则判断该点是否为原树直径中点。

T3 划分

如果有长度 K0 前缀,最优情况就是后面的全放一段,计算前面划分出 K1 段的方案数。

否则就一定是找最大的长度为 nk+1 的一段(整段),再把其它的单个划分出来(散段),枚举起始点,取模后如何比较两个二进制数的大小?

可以发现类似于比较字典序大小,一个方法是二分哈希找到两个 01 串的最长公共前缀,比较其下一位即可,并且我们只需要比较整段而不需要比较散段,因为一个字符 1 再整段里的贡献是 2i1,而在散段里的贡献只能是 1,所以加上散段后贡献不会变劣,但是存在 20=1 的特殊情况,即最后一位在整段和散段的贡献相同, 我们的处理就是只要最长公共前缀长度 nk 则判相等,累计方案数,否则再进行比较即可,时间复杂度 Θ(nlogn)

写自然溢出会被卡,不要写自然溢出。

T4 灯笼

10.27 模拟4 A层联测19

40+50+15+50=155pts rk19

T1没签上,考场上写模拟自己构死了,后来感觉是根号分治,但是没想到正解是 O(1) 式子。

T2正解思路但是 n2 实现,我nc

T3可以转换成费用流,但是没有学

T4糊了个 n2 的假贪心,拿了sub2 的80%。

T1 购买饮料

首先先特判一开始买不到 a 瓶和可以无限买的情况,然后我们考虑把 a 瓶饮料当做一个套餐,假如钱数一直大于 ax,那么一个套餐的价格为 axb,我们考虑加上钱数大于 ax 的限制,那么我们能购买的套餐数为 naxaxb+1 个,则获得的总钱数为 (naxaxb+1)b+n,瓶数则再除一个 x 即可,复杂度 Θ(Q)

T2 多边形

我们可以把三角剖分看成不断在当前多边形上切下来一个三角形,且要满足顶点颜色互不相同(简称为限制条件),容易发现题目限制保证一定有解,那么我们考虑不断寻找原序列的符合要求的连续三元组,如三元组 RGB,一次切三角形操作就相当于从原序列删除 G,再把 RB 连边。

考虑使用链表实现,便于删除和首位相连,对于一个满足限制条件的位置,删除该位置后前面的点可能再次相连形成合法三元组,我们就一直删前面的直到不满足限制条件,再去下一个位置删。

注意要先特判有一个颜色只出现一次的情况,因为我们可能运气不好一上来直接把这个点给删了,然后就判不合法了。

但是所有颜色都出现两次以上是保证合法的:考虑极端情况,R 颜色只出现了两次,我们一开从第一个 R 开始删,在链表中只剩一个 R,可以发现其他位置一定是 GBGB 的循环,那么我们在枚举到 R 的上一个位置就可以把整个序列删干净而不动这个仅剩的 R

核心代码:

while(siz>3){
while(check(pos)&&siz>3){
cout<<lst[pos].pre<<' '<<lst[pos].back<<'\n';
lst[lst[pos].pre].back=lst[pos].back;
lst[lst[pos].back].pre=lst[pos].pre;
siz--;
cnt[a[pos]]--;
pos=lst[pos].pre;
}
pos=lst[pos].back;
}

T3 二分图最大权匹配

咕,学流去了。

T4 飞毯

10.28 模拟5 Furry!场

100+75+10=185pts rk15

前两个小时脑子一直很懵,洗了把脸回来才有了点状态。

T1 签到

T2 考场上写了个空间复杂度 m2 的做法,正解思路挺自然的。

T3 暴力,还有 15pts 的卡特兰数部分分,补了。

T1 x

考虑三个数 a,b,c(ab)c=abca(bc) 两种顺序,发现当 b2bcbc 恒成立,所以特判出 b=1 的情况从前往后算一定最优,遇见 1 就直接 break,因为后面的数可以贡献到 1 上相当于没有贡献。

T2 u

75pts:太抽象了不说了。

首先最小生成树权值不变即选的边不变。证明考虑对于每一条未确定的边 i,能够替换一条树边当且仅当 wimaxjuivi{wj},又因为边权互不相同所以最小生成树权值只可能减小而不可能被替换。

我们从小到大考虑每一个权值 val,如果我们只考虑原图上权值 val 的确定的边,那么会形成若干个连通块,我们考虑此时所有权值 val 的未确定的边,发现它一定不能连接两个连通块,只能选择每个连通块内完全图上未确定的边。

发现最后答案为 Yes 当且仅当对于每一个 val 都需要满足上述限制,于是我们动态维护每个连通块的 siz×(siz1)2 即完全图的边数,已确定的边数 tot,小于 val 的未确定的边的数量 cntcntsiz×(siz1)2tot

设两个连通块大小分别为 x,y,则合并后变化量为 (x+y)×(x+y1)2x×(x1)2y×(y1)2=x×y

核心代码:

int main(){
....
sort(e+1,e+1+m);
ll sum=0/*每个联通块内完全图的边数和*/,tot=0/*每个连通块内已确定的边数*/;
for(int i=1;i<=m;i++){
if(e[i].w-i>sum-tot)return !puts("No");
int fx=find(e[i].u),fy=find(e[i].v);
if(fx==fy)continue;
if(siz[fx]<siz[fy])swap(fx,fy);
for(auto y:E[fy]){
if(find(y)==fx)tot++;
else E[fx].emplace_back(y);
}
sum+=1ll*siz[fx]*siz[fy];
fa[fy]=fx;
siz[fx]+=siz[fy];
}
puts("Yes");
}

T3 a

序列单调

单调递增为队列,方案数就是 1,单调递减为栈,方案数为卡特兰数。

Ai 互不相同

首先因为是小根堆,那么当我们把序列最大值(为 n,设在 A 序列中位置为 p)放入 B 中时,那么在 p 之前的所有数需要都放入堆中且都从堆提到 B 中才能轮到 p(此时堆为空),那么 A1Ap 的元素种类应该等于 B1Bp,同理 p+1 往后也相等,也就是说此时我们的问题转化为了两个独立的部分 [1,p1][p+1,n],我们只需要统计这两个子序列的方案数即可,就可以一直递归下去了。

这就很像区间 DP,我们设 dpl,r,i 表示 [l,r] 区间中只 Aji 形成的子序列的答案。

  • i[l,r] 中没有出现,那么 dpl,r,i=dpl,r,i1

  • 否则设 i 出现的位置为 pos ,那么枚举 posB 中出现的位置 j,就有:dpl,r,i=j=posrdpl,j,i1×dpj+1,r,i1

转移即可,记得将不合法情况也初始化为 1,答案为 dp1,n,n

无特殊限制

我们考虑怎么解决相等元素的问题。

我们发现每个元素的下标互不相同,对于若干个相同元素,一定是下标小的先入队列,那么其在队列里的位置也在前面,那么它也是先被放入 B 中的,也就是元素是遵循先进先出原则的,我们据此先按元素大小排序,若相等则按下标排序,按照排序结果类似离散化一样给每一个元素重标号,再进行如上的区间 DP 即可。

复杂的 Θ(n4)

Code:

点击查看代码
int a[M];
pair<int,int> b[M];
bool operator<(const pair<int,int> x,const pair<int,int> y){
if(x.first==y.first)return x.second<y.second;
return x.first<y.first;
}
ll dp[M][M][M];
int main(){
int n=read();
for(int i=1;i<=n;i++){
a[i]=read();
b[i].first=a[i];
b[i].second=i;
}
sort(b+1,b+1+n);
for(int i=1;i<=n;i++){
a[b[i].second]=i;
}
for(int i=0;i<=n+1;i++)
for(int j=0;j<=n+1;j++)
for(int k=0;k<=n+1;k++)
dp[i][j][k]=1;
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
for(int i=1;i<=n;i++){
int pos=-1;
for(int j=l;j<=r;j++){
if(a[j]==i){
pos=j;
break;
}
}
if(pos==-1)dp[l][r][i]=dp[l][r][i-1];
else{
dp[l][r][i]=0;
for(int j=pos;j<=r;j++){
if(a[j]<=i)
dp[l][r][i]=(dp[l][r][i]+(dp[l][j][i-1]*dp[j+1][r][i-1])%mod)%mod;
}
}
}
}
}
cout<<dp[1][n][n];
return 0;
}

T4 y

咕。

10.30 模拟6 A层联测27

90+20+30=140pts rk17

T1 爆搜题,但是少考虑情况挂了10pts

T2 想到了基环树,但是不会维护,最后打了暴力和环的特殊性质,但是暴力因为数据不合法挂了10pts,环因为忘删暴力数组 RE 挂了 30pts。。。

T3 写了30pts的DP,这种数据结构优化的题我总是不会。

T4 前10min才发现暴力能打,但是没时间了。

T1 机器人

发现 n20,爆搜就行了,也可以写状压,对于每一个指令有执行和不执行两种选择,执行移动,不执行留在原地,需要注意如果我们已经确定了一个操作在该位置执行,那么机器人之后走到同样的位置时的操作一定也是执行的,不执行同理,即同一个位置不能前后矛盾。

T2 旅行

30pts 暴力

我们考虑将所有边按权值分组,对于所有值域相同的边,使用并查集暴力求出连通块个数,对于每次修改操作,在原边权集合暴力删除该边并重新计算连通块,在新边权集合加入该边并重新计算连通块,时间复杂度 Θ(?)

另 30pts 特殊性质

断环为链,将所有边权塞到序列上,用线段树维护区间连通块个数,注意当首尾颜色相同时答案要减一。

正解

先考虑树的情况,给每个节点开一个 map 表示所有连该点的边的颜色种类和个数。

  • 对于一条边 wi,若 uv 之前都有其它的边权为 wi 与其相连,则会把之前的两个连通块合并为一个,令 ans 减一。

  • 若只有其中一个点有相连的,则会拓展该连通块,ans 不变。

  • 若都没有相连的,则会产生一个新的连通块,令 ans 加一。

n 个点 n 条边,是基环树,分环上的边和其他的边,其他的边正常做,环上的边考虑特殊情况:

若该环都为同一种颜色,则加入最后一条边时两边一直是一个连通块,但根据操作 1 会令答案减一,所以再加回来,令 ans 加一。

T3 点餐

先考虑计算一个特定的 k 的答案。

我们按 b 值从大到小排序,枚举我们选的最大的 bi,设位置为 x(xk),则最优一定是在前 x1ai 中再选出 k1 个最小的加入贡献,问题转化为求区间的 k 小值之和,主席树维护,对于所有位置 x 计算贡献,我们设为 w(k,x)k 的答案就是取最小值。

然后我们考虑解决 [1,n] 的所有 k

f(k) 表示 k 取到最小值时的 x,对于两个决策点 x,y(x<y),令 w(k,x)w(k,y),那么当 k 增大时,所有 x 能选的 anewy 都是能选的,所以新选的 axay,即 w(k,x)w(k,y),(k<k),即 f(i)f(i+1) 对于任意 i+1n 恒成立。

所以决策点是具有单调性的,序列分治,每次在 [lx,rx] 区间查询 kmid 的答案,分治复杂度 nlogn,总时间复杂度 Θ(nlog2n)

点击查看代码
const int M=200010;
const ll inf=1ll<<50;
struct node{
ll a,val,b;
friend bool operator<(const node x,const node y){return x.b<y.b;}
}o[M];
int a[M],root[M];
namespace Segment_Tree{
struct node{
int l,r,cnt;
ll sum;
}t[M<<5];
int cnt(0);
int update(int pre,int l,int r,int x,ll val){
int rt=++cnt;
t[rt]=t[pre];
t[rt].sum+=val;
t[rt].cnt++;
if(l==r)return rt;
int mid=(l+r)>>1;
if(x<=mid)t[rt].l=update(t[pre].l,l,mid,x,val);
else t[rt].r=update(t[pre].r,mid+1,r,x,val);
return rt;
}
ll query(int pos,int l,int r,int k){
if(l>r)return 0;
if(l==r){
if(t[pos].cnt==0)return 0;
else return 1ll*t[pos].sum/t[pos].cnt*k;
}
int x=t[t[pos].l].cnt;
int mid=(l+r)>>1;
if(x>=k)return query(t[pos].l,l,mid,k);
else return t[t[pos].l].sum+query(t[pos].r,mid+1,r,k-x);
}
}
ll ans[M];
int n;
void testify(int lk,int rk,int lx,int rx){
if(lk>rk)return;
if(lx>rx)return;
int mid=(lk+rk)>>1,pos(0);
ll mn=inf;
for(int i=max(lx,mid);i<=rx;i++){
ll tmp=Segment_Tree::query(root[i-1],1,n,mid-1)+o[i].val+o[i].b;
if(tmp<mn){
mn=tmp;
pos=i;
}
}
ans[mid]=mn;
if(lk==rk)return;
testify(lk,mid-1,lx,pos);
testify(mid+1,rk,pos,rx);
return;
}
main(){
n=read();
for(int i=1;i<=n;i++) a[i]=o[i].a=read(),o[i].b=read();
sort(a+1,a+1+n);
int len=unique(a+1,a+1+n)-(a+1);
for(int i=1;i<=n;i++)o[i].a=lower_bound(a+1,a+1+len,o[i].a)-a;
for(int i=1;i<=n;i++)o[i].val=a[o[i].a];
sort(o+1,o+1+n);
for(int i=1;i<=n;i++){
root[i]=root[i-1];
root[i]=Segment_Tree::update(root[i],1,n,o[i].a,o[i].val);
}
for(int i=1;i<=n;i++)ans[i]=inf;
testify(1,n,1,n);
for(int i=1;i<=n;i++)cout<<ans[i]<<'\n';
return 0;
}

T4 无穷括号序列

咕。

10.31 模拟7 A层联测28

100+15+20+15=150pts rk9

T1 签到题

T2 正解真抽象,打暴力走人,但是没看出来 n173n 复杂度。(今天刚知道枚举子集是 3n

T3 只会打暴力,正解是点分树套点分治?

T4 没打完特殊性质,前几个部分分复杂度假了,复杂度 Qn3

T1 路径

首先图是 DAG 那就多少根拓扑排序沾点关系。

可以先设个超级源点连向所有入度为 0 的点(好理解)

我们设 ansx 表示所有从源点到 x 的所有简单路径中包含关键点的个数的最大值,然后在拓扑的时候顺便转移。

最后我们只需要判断是否有点的 ansx=k 即可。

T2 异或

sub1,2 直接爆搜。

正解是一些很神奇的东西。。

首先我们可以吧原序列转化为异或差分序列,设为 B,则 Bi=AiAi1,我们就将区间操作转化为单点操作,对于后缀的区间修改 [l,n],我们转化为 l 处的单点修改,对于非后缀的 区间修改 [l,r],我们转化为 lr+1 处的单点修改,然后我们的任务就转化为令差分序列全变为 0

然后我们抽象地转化,对于单 l 的修改,我们从 ll 连一条无向边即自环,对于 lr+1 的的修改,我们从 lr+1 连一条无向边。

然后区间就被分成了若干个连通块,操作次数就是所有边数,我们考虑如何用最少的边让每个点都有连边。

结论:若一个连通块的大小为 x,则该联通块的边数只可能是 xx1

  • 对于 x1 条边,它是一颗无根树,且满足所有位置的 Bi 异或和为 0

  • 对于 x 条边,它一定是一颗无根树+某个位置的一个自环。

证明:我对于一个 x1 条边的连通块,我们尝试从所每个叶子结点开始删(即让该点 Bi 变为 0),那么就是异或它自己的权值的一个操作,然后这个点就没了,我们一直这样删下去,最后肯定会剩一个点,若当前点的 Bi 已经自己变为 0,即所有点异或为 0,那就不用删这个点,即只用删 x1 次,否则我们要再删一次,即 x 次。

然后我们考虑答案是什么,先假设所有点都是自环,此时操作数为 n,然后考虑把一些异或为 0 的点连成一棵树,设大小为 siz,则它的贡献从 siz 变为 siz1,总答案减一,我们设我们找到了 sum 个异或和为 0 的连通块,则答案即 nsum

现在我们想最大化这个 sum,即最大化异或和为 0 且每个点只包含于一个序列的序列数,数据范围较小,考虑使用状压解决,预处理出来每个集合是否异或和为 0,枚举子集转移即可。

复杂度:

枚举子集的复杂度是 3n 的,n17 正好。

证明: i=0n(ni)2i=3n

点击查看代码
const int M=18;
ll a[M],b[M];
bool zero[(1<<18)+10];
int dp[(1<<18)+10];//考虑s这个子序列能划分出异或和为零的连通块的最大个数
int main(){
int n=read();
for(int i=1;i<=n;i++)a[i]=read();
b[1]=a[1];
for(int i=2;i<=n;i++)b[i]=a[i]^a[i-1];
zero[0]=1;
for(int s=0;s<(1<<n);s++){
ll sum=0;
for(int i=1;i<=n;i++)if((s>>(i-1))&1)sum^=b[i];
if(sum==0)zero[s]=1;
}
for(int s=0;s<(1<<n);s++){
dp[s]=max(dp[s],1*zero[s]);
for(int t=s;t;t=(t-1)&s){
if(t==s)continue;
dp[s]=max(dp[s],dp[t]+zero[s-t]);
}
}
cout<<n-dp[(1<<n)-1];
}

T3 距离【补】

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define il inline
#define ll long long
il int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int M=2e5+10;
const ll inf=1ll<<50;
struct EDGE{
int v,nxt;
ll w;
}e[M<<1];
int head[M],cnt(0);
il void add(int u,int v,ll w){
e[++cnt].v=v;
e[cnt].w=w;
e[cnt].nxt=head[u];
head[u]=cnt;
}
struct node{
int opt,x,y;
}o[M];
int Fa[M],siz[M],maxp[M];
ll dep[M];
bool vis[M];
int tot,rt;
namespace HLD{
int top[M],son[M],fa[M];
void dfs1(int x,int fat){
fa[x]=fat;
siz[x]=1;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(y==fat)continue;
dep[y]=dep[x]+e[i].w;
dfs1(y,x);
siz[x]+=siz[y];
if(!son[x]||siz[y]>siz[son[x]])son[x]=y;
}
}
void dfs2(int x,int tpx){
top[x]=tpx;
if(!son[x])return;
dfs2(son[x],tpx);
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);
}
}
il int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
ll get_dis(int x,int y){
return dep[x]+dep[y]-2ll*dep[lca(x,y)];
}
}
void find_root(int x,int fa){
siz[x]=1;maxp[x]=0;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(vis[y]||y==fa)continue;
find_root(y,x);
siz[x]+=siz[y];
maxp[x]=max(maxp[x],siz[y]);
}
maxp[x]=max(maxp[x],tot-siz[x]);
if(!rt||maxp[x]<maxp[rt])rt=x;
}
void divide1(int x,int fa){
Fa[x]=fa;
vis[x]=1;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(vis[y]||y==fa)continue;
rt=0;
tot=siz[y];
find_root(y,0);
divide1(rt,x);
}
}
ll ans[M],mn[M];
vector<int>Q[M],save;
il void update(int x,int a,int b){
ll dis1=HLD::get_dis(x,a);
int now=b;
while(now){
mn[now]=min(mn[now],dis1+HLD::get_dis(now,b));
now=Fa[now];
}
return;
}
il ll query(int xx,int x,int y){
ll ans=inf,dis1=HLD::get_dis(x,xx);
int now=y;
while(now){
ans=min(ans,dis1+mn[now]+HLD::get_dis(now,y));
now=Fa[now];
}
return ans;
}
il void insert(int k){
int now=o[k].x;
while(now){
Q[now].emplace_back(k);
now=Fa[now];
}
}
il void clear(int x){
int now=x;
while(now){
mn[now]=inf;
now=Fa[now];
}
}
il void calc(int x){
save.clear();
for(auto const &q:Q[x]){
if(o[q].opt==1){
update(x,o[q].x,o[q].y);
save.emplace_back(o[q].y);
}
else{
ans[q]=min(ans[q],query(x,o[q].x,o[q].y));
}
}
for(auto const &q:save)clear(q);
}
void divide2(int x){
vis[x]=1;
calc(x);
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(vis[y])continue;
tot=siz[y];
rt=0;
find_root(y,0);
divide2(rt);
}
}
int main(){
freopen("distance.in","r",stdin);
freopen("distance.out","w",stdout);
int n=read(),m=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
add(u,v,w);
add(v,u,w);
}
HLD::dfs1(1,0);
HLD::dfs2(1,1);
tot=n;
find_root(1,0);
divide1(rt,0);
for(int i=1;i<=n;i++)vis[i]=0;
for(int i=1;i<=n;i++)mn[i]=inf;
for(int i=1;i<=m;i++)ans[i]=inf;
for(int i=1;i<=m;i++){
o[i].opt=read(),o[i].x=read(),o[i].y=read();
insert(i);
}
tot=n;
rt=0;
find_root(1,0);
divide2(rt);
for(int i=1;i<=m;i++){
if(o[i].opt==1)continue;
if(ans[i]>=inf)puts("-1");
else cout<<ans[i]<<'\n';
}
return 0;
}

T4 花之舞

11.1 模拟8 A层联测29

20+0+0+0=20pts rk39

今天打了一场模拟赛,又垫底了。

T1 线段树写错一个地方,大样例没测出来,挂 80pts

T2 杀币题,暴力调了俩小时没调出来,心态炸了。

T3T4基本没看题。

T1 集合

考场打的序列分治加线段树维护最大连续段长度,复杂度 nlog2n,加完优化比 nlogn 跑的还快。

正解是双指针,我们发现当区间左端点右移时,右端点也单调右移,这个比较好看出来,然后就用线段树维护值域上的最长连续段长度,好维护。

push_up函数:

il void push_up(int k){
t[k].sum=max({t[k<<1].sum,t[k<<1|1].sum,t[k<<1].sr+t[k<<1|1].sl});
t[k].len=t[k<<1].len+t[k<<1|1].len;
if(t[k<<1].sum==t[k<<1].len)t[k].sl=t[k<<1].sum+t[k<<1|1].sl;
else t[k].sl=t[k<<1].sl;
if(t[k<<1|1].sum==t[k<<1|1].len)t[k].sr=t[k<<1|1].sum+t[k<<1].sr;
else t[k].sr=t[k<<1|1].sr;
}

T2 差后队列

真难调,调了整整一下午。。。

n2 暴力:

直接模拟队列,表示所有可能在队列里元素集合,记数组 inqi 表示该元素留在队列里的该概率,siz 表示当前队列的实际大小。

对于每个 pop 操作,若集合大小为 1,就是弹出最大值,然后清空队列;若集合大小为 2,则弹出的元素是唯一确定的,所以把队列的清空只留下最大值,更新每个弹出元素 j 的答案 ansj+=inqj×i,更新当前位置的答案 ansi+=aj×inqj;若集合大小大于 2,则集合中的每个数被选到的概率都是 1siz1,更新每个非最大值元素的答案 ansj+=i×inqj×1siz1inqj=(11siz1),更新当前位置的答案 ansi+=aj×inqj×1siz1

正解:

发现入队的答案是受其后面位置的影响,而出队操作受其前面位置的影响,先考虑从前往后更新出队操作的答案。

设当前位置之前不含最大值的所有元素的期望权值和为 now,实际队列大小为 siz,则遇到一个入队操作后 now=siz1siz×now,遇到一个出队操作则直接给当前位置答案赋值。

然后我们考虑从后往前更新入队操作的答案。

我们设 now 表示当前位置之后的所有出队横坐标的期望和。那个遇到一个入队操作时,直接更新它的答案;当遇到一个出队操作时,从当前的 sizi 个数中选出一个数,在该位置被删除,期望是 isizi,否则会在后面的位置删除,概率是 sizi1sizi,则 now=isizi+sizi1sizi×now

点击查看代码
const int M=1e6+10;
const int mod=998244353;
pair<int,int> a[M];
il int fast_pow(int x,int a){
int ans=1;
while(a){
if(a&1)ans=(ans*x)%mod;
x=(x*x)%mod;
a>>=1;
}
return ans;
}
int siz[M],ans[M],fm[M];
main(){
int n=read();
for(int i=1;i<=n;i++){
a[i].first=read();
if(a[i].first==0)a[i].second=read();
}
int nowsiz=0,mx=0,pos=0,now=0;
for(int i=1;i<=n;i++){
if(a[i].first==0){
nowsiz++;
if(!mx){
mx=a[i].second;
pos=i;
}else if(a[i].second>mx){
now=(now+mx)%mod;
fm[i]=pos;
mx=a[i].second;
pos=i;
}else{
now=(now+a[i].second)%mod;
fm[i]=i;
}
}
else if(a[i].first==1){
nowsiz--;
if(nowsiz==0){
ans[i]=a[pos].second;
ans[pos]=i;
mx=pos=0;
}else{
ans[i]=(now*fast_pow(nowsiz,mod-2))%mod;
now=((nowsiz-1)*fast_pow(nowsiz,mod-2)%mod)*now%mod;
}
}
siz[i]=nowsiz;
}
now=0;
for(int i=n;i>=1;i--){
if(a[i].first==0)ans[fm[i]]=now;
else{
if(siz[i]){
now=((i*fast_pow(siz[i],mod-2))%mod+(((siz[i]-1)*fast_pow(siz[i],mod-2))%mod*now)%mod)%mod;
}
}
}
for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
return 0;
}

T3T4咕

11.2 模拟9

100+40+50+0=190pts rk27

T1 签到题

T2 顺着正解思路想的,差点想出来,后来去想DP了。

T3 线段树又又打错了一个地方痛失 50pts

T4 没时间看了。

快退役啦,珍惜为数不多的学 OI 时间吧。。

T1 上海

n2k 的倍数,则 n2 的质因子集合完全包含 k 的质因子集合,则 n 的质因子集合完全包含 k 的质因子集合。

nkn 一定能整除 k,所以 n<k

暴力是直接枚举 n

考虑将 k 分解质因数,时间复杂度 n,若次数都为 1 则一定无解,否则让 n 每个质因数选至少一半以上才能保证 n2 整除 k

T2 华二

发现值域小的离谱,就按每个权值考虑。

对于 ai1,5,7,它们与其他数都互质,可以和所有数交换位置,即在序列中的位置是任意的,我们放到最后考虑。

然后在剩余数字中我们发现 6 是和其他数都不互质的,也就是 6 在序列中的位置是固定,相当于把序列分成若干段,段和段之间不能交换位置,方案数直接相乘。

然后在每一段中只剩 2,3,4,8,9 这些数,发现 2,4,8 是不互质的,3,9 也是不互质的,它们在序列中的相对顺序是确定,但两组数中之间是互质的,可以任意交换,这部分的方案数是 (s1+s2s1)

然后我们分别把 1,5,7 插入序列,设三个数字的个数分别为 S1,S2,S3,则方案数是:

(nnS1S2S3)×(S1+S2+S3S1)×(S2+S3S2)×(S3S3)=n!(nS1S2S3)!×S1!×S2!×S3!

T3 高爸

Slope Trick ?

我们先考虑单次查询。

设变成的力量值为 y,则

i=1r{a×(yxi),xiyb×(xiy),xi>y

对于每个 i,我们容易发现它是一个斜率不等的绝对值函数,是凹函数,凹函数加凹函数还是凹函数,即单峰函数,使用三分查找。

现在我们需要求出对于一个特定 y 的上式子的值,以 xiy 为例:

(1)ia×(yxi)(2)=a×(iyixi)(3)=a×(cnt×ysum)

如上式线段树维护 y 的个数和 xi 的和即可。

具体的,将权值离散化作为下标使用,维护区间最大值,当查询的 yls.mx,去左区间,否则去右区间。

大于那部分直接拿总个数和总权值和减 cntsum 即可。

其实可以直接用权值树状数组,我是sb。

然后每次查询就是单点修改。

时间复杂度 Θ(nlog2n)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define il inline
#define ll long long
il ll read(){
ll x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int inf=1ll<<30;
const int M=1e5+10;
int val[M],o[M],pos[M];
int n,a,b;
ll now_sum;
namespace Segment_Tree{
struct node{
int mx;
int cnt;
ll sum;
}t[M<<2];
void build(int k,int l,int r){
if(l==r){
t[k].cnt=1;
t[k].mx=t[k].sum=o[l];
return;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
}
il void push_up(int k){
t[k].cnt=t[k<<1].cnt+t[k<<1|1].cnt;
t[k].sum=t[k<<1].sum+t[k<<1|1].sum;
t[k].mx=max(t[k<<1].mx,t[k<<1|1].mx);
}
void update(int k,int l,int r,int pos,ll val){
if(l==r){
t[k].cnt++;
t[k].sum+=val;//考场上写成t[k].sum=val挂了。。
t[k].mx=val;
return;
}
int mid=(l+r)>>1;
if(pos<=mid)update(k<<1,l,mid,pos,val);
else update(k<<1|1,mid+1,r,pos,val);
push_up(k);
}
pair<ll,int> query(int k,int l,int r,ll x){
if(l==r){
if(t[k].sum<=x) return make_pair(t[k].sum,t[k].cnt);
else return make_pair(0,0);
}
int mid=(l+r)>>1;
if(x<=t[k<<1].mx)return query(k<<1,l,mid,x);
else{
pair<ll,int> ans=query(k<<1|1,mid+1,r,x);
return make_pair(t[k<<1].sum+ans.first,t[k<<1].cnt+ans.second);
}
}
}
ll calc(int x,int r){
pair<ll,int> ans=Segment_Tree::query(1,1,n,x);
ll tmp=(1ll*ans.second*x-ans.first)*a+(now_sum-ans.first-1ll*(r-ans.second)*x)*b;
return tmp;
}
int main(){
n=read(),a=read(),b=read();
for(int i=1;i<=n;i++)o[i]=val[i]=read();
sort(o+1,o+1+n);
int len=unique(o+1,o+1+n)-(o+1);
for(int i=1;i<=n;i++)val[i]=lower_bound(o+1,o+1+len,val[i])-o;
int mn=inf,mx=0;
for(int r=1;r<=n;r++){
mn=min(o[val[r]],mn);
mx=max(o[val[r]],mx);
now_sum+=o[val[r]];
Segment_Tree::update(1,1,n,val[r],o[val[r]]);
int L=mn,R=mx;
ll ans=0;
while(L<=R){
int k=(R-L)/3;
int mid1=L+k,mid2=R-k;
ll w1=calc(mid1,r),w2=calc(mid2,r);
if(w1>w2){L=mid1+1;ans=w2;}
else {R=mid2-1;ans=w1;}
}
cout<<ans<<'\n';
}
return 0;
}

T4 金牌

先推推式子:

(4)ans=iSujSv2disi,j(5)=iSujSv2disi,u+disu,v+disv,j(6)=iSujSv2disi,u2disj,v2disu,v(7)=2disu,viSujSv2disi,u2disj,v(8)=2disu,viSu2disi,ujSv2disj,v

2disu,v 可以直接 lca 求,这里需要用树剖,倍增会被卡常。

iSu2disi,u 这个东西比较难搞,分子树内的情况和子树之外的情况。

  • sum1 表示子树内的这个答案,直接 DFS 回溯的时候求 sum1[x]=jsonx2(sum1[y]+1)
  • sum2 表示从父亲节点转移过来的答案,换根求:

sum2[x]=2(sum2[fax]+sum1[fax]2sum1[x]2)+2

那么对于每次查询点对 (x,y),分三种情况(两种本质一样):

  • xlca,则 ans=2disx,y×(sum1[y]+1)×sum2[slca]

  • ylca,则 ans=2disx,y×(sum1[x]+1)×sum2[slca]

  • xy 都不是 lca,则 ans=2disx,y×(sum1[x]+1)×(sum1[y]+1)

其中 slca 为链上 lca 下面的那个点,树剖咋求呢?

首先钦定 ylca,那么 x 跳到 y 所在的重链上有两种情况:

  • 直接跳到 y 上,那么 slca 就是 x 上次跳时的 top,我们记录 from 表示这个东西(类似于记录路径)。
  • 跳到 y 以下的点,因为重链 dfs 序连续,直接返回 pos[dfn[y]+1]

做完了。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define il inline
#define ll long long
il ll read(){
ll x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int inf=1ll<<30;
const int M=1e6+10;
const int mod=998244353;
ll sum1[M]/*x点子树的答案*/,sum2[M];/*x点从父亲转移来的答案*/
struct node{
int v,nxt;
}e[M<<1];
int head[M],cnt(0);
il void add(int u,int v){
e[++cnt].v=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
int _2[M],son[M],top[M],siz[M],dep[M],fa[M],dfn[M],pos[M],tim(0);
namespace HLD{
void dfs1(int x,int fat){
fa[x]=fat;
siz[x]=1;
dep[x]=dep[fat]+1;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(y==fat)continue;
dfs1(y,x);
siz[x]+=siz[y];
if(!son[x]||siz[y]>siz[son[x]])son[x]=y;
}
}
void dfs2(int x,int tpx){
dfn[x]=++tim;
pos[tim]=x;
top[x]=tpx;
if(!son[x])return;
dfs2(son[x],tpx);
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
int slca(int x,int y){
int fm;
while(top[x]!=top[y]){
fm=top[x];
x=fa[top[x]];
}
if(x==y)return fm;
else if(fa[x]==y)return x;
else return pos[dfn[y]+1];
}
}
void dfs1(int x){
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(y==fa[x])continue;
dfs1(y);
sum1[x]=(sum1[x]+sum1[y]*2+2)%mod;
}
}
void dfs2(int x){
if(fa[x])sum2[x]=((sum2[fa[x]]+sum1[fa[x]]-sum1[x]*2-2+mod)%mod*2+2)%mod;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].v;
if(y==fa[x])continue;
dfs2(y);
}
}
int main(){
int n=read();
for(int i=1;i<n;i++){
int u=read(),v=read();
add(u,v);
add(v,u);
}
_2[0]=1;
for(int i=1;i<=n;i++)_2[i]=(2ll*_2[i-1])%mod;
HLD::dfs1(1,0);
HLD::dfs2(1,1);
dfs1(1);
dfs2(1);
int q=read();
while(q--){
ll ans=0;
int x=read(),y=read();
int lca=HLD::lca(x,y);
int dis=dep[x]+dep[y]-2*dep[lca];
if(lca==x){
int slca=HLD::slca(y,x);
ans=_2[dis-1];
ans=(ans*(sum2[slca]*(sum1[y]+1)%mod)%mod)%mod;
}
else if(lca==y){
int slca=HLD::slca(x,y);
ans=_2[dis-1];
ans=(ans*((sum1[x]+1)*sum2[slca]%mod)%mod)%mod;
}
else{
ans=_2[dis];
ans=(ans*((sum1[x]+1)*(sum1[y]+1)%mod)%mod)%mod;
}
cout<<(ans+mod)%mod<<'\n';
}
return 0;
}

11.3 模拟10 A层联测23

100+0+20+0=120pts rk4

概率期望 _ _ _ _!

T1 博弈论,结论很简单,考场上打博弈图找的规律。

T2 概率期望。。 不会一点

T3 概率期望。。。 暴力20pts

T4 概率期望。。。。铁假了

今天早上把震波调出来了,竟然是树状数组越上界了。。

T1 游戏

首先可以确定的是如果集合为空则是后手必胜。

否则集合中是一定有 1 的。那么先手如果选其它数能够达到先手必胜局面,则直接就取该数。

否则因为删去 1 不会对后面的数造成影响,那么先手就可以删去 1,把当前的必败局面转移给后手。

然后先手就无敌了。

T2 涂鸦

60pts:

我们发现该矩阵只有 2nm 种状态,对于每一种状态,我们设 fs 表示从该状态转移到最终状态的期望代价。

然后我们发现对于每一个状态的转移方程都是形如:

fx=1p(f1+val1)+1p(f2+val2)+1p(f3+val3)+...+1p(fT+valT)

其中 p=2nm 即选中的概率。

然后这个东西就很能高斯消元,把形式转换一下就行了。

注意我们到最终状态之后就已经停了,所以不能转移最终状态。

然后答案就是 fst 即初始状态,而不是 fed 我就这么写错的

100pts:

考虑如何减少状态数。

我们发现如果你当前修改的位置右下方存在一个位置的颜色和最终状态的颜色不同,那么该操作一定会被之后的一个操作完全覆盖掉,那么这个操作就没用了。

所以真正有用的操作是一个形如阶梯状从左下角到右上角的点集,我们把所有的阶梯设为状态,状态数其实就是从左下角走到右上角的路径数,是 (n+mn)

考虑转移。对于一个已有状态,枚举当前修改的位置,可以发现只有该位置在阶梯上才能继续拓展该阶梯,否则只能转移到它自己。若可以拓展该阶梯,则转移到到拓展后的阶梯,即修改后的矩阵和最终矩阵新产生的相同的部分,而且要保证不破坏原阶梯的形状。

建议画画图理解。

然后就是一样的列方程组,高斯消元求解。

T3

只会暴力。

题意很抽象。

暴力枚举所有可能的 c 个位置,对于每一个组合, “为了区分起始点所需走的步数”也就是说按照题目规定的行走规则,需要走多少步才能使得这 c 个路径两两互不相同。

然后暴力模拟就行了,把路径哈希一下在哈希表上查即可。

T4

咕。什么东西。。

posted @   CCComfy  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示