LG 杂题
P10868 [HBCPC2024] Points on the Number Axis B
给你一个不降序列 \(a\),求每次随机将相邻的两个数替换成他们的平均数,最后剩下的一个数的期望。
\(n\le 10^6,a_i<998244353\)
拜谢巨佬 xiezheyuan
非常巧的一个题,可以说和 BBQ Hard 思路相反。
对于一个数 \(a_i\),对于一种情况 \(t\),它对答案贡献的系数为 \(2^{-k_t}\),其中 \(k_t\) 是这种情况下它被选取的次数。由于期望可加,所以我们可以对于每个 \(a_i\),算出它对答案的贡献系数和 \(\sum\limits_{t\in S}2^{-k_t}\)。设 \(f(i,j)\) 表示当前(合并了)前面有 \(i\) 个数(相当于现在在 \(i+1\)),后面有 \(j\) 个数的贡献系数。则答案为 \(\sum\limits_{i=1}^n f(i,n-i-1)\)。
考虑递推,若合并的数在前/后面 \(i/j\) 个数中(有 \(i-1/j-1\) 种选法),则当前数的贡献系数不变,若选定了当前数,则它可以和前/后面相邻的数合并,且系数 \(\times \frac{1}{2}\)。
综上,递推式为
整理一下
两边乘 \((i+j)!\) 得
这个形式很漂亮,不妨设 \(g(i,j)=(i+j)!f(i,j)\),则
有 BBQ Hard 那味了。联想一下,这等价于从 \((0,0)\) 走到 \((i,j)\) 的方案数的路径权值(由于不带95:56权时为 \(1\),可以看做是把每边边权乘起来)之和,限制向右或上走,且横向边权为 \((i-\frac{1}{2})\),纵向边权为 \((j-\frac{1}{2})\)。可以发现每种方案的路径权值固定为 \(\prod\limits_{k=1}^{i}(i-\frac{1}{2})\prod\limits_{k=1}^{j}(j-\frac{1}{2})\),而方案数为 \(\binom{i+j}{i}\),所以上式可化简为
预处理组合数和积即可。时间复杂度 \(O(n)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
const int mod=998244353;
const int maxn=1e6+3;
using namespace std;
const int N=2003;
int n,a[maxn];
int inv[maxn],fac[maxn],invf[maxn],g[maxn],fack[maxn];
int C(int a,int b){
if(a<b) return 0;
return fac[a]*invf[b]%mod*invf[a-b]%mod;
}
int qpow(int a,int b){
int res=1;
for(;b;a=a*a%mod,b>>=1) if(b&1) res=res*a%mod;
return res;
}
signed main(){
cin>>n;
fac[0]=fac[1]=inv[1]=1;
for(int i=2;i<=n;i++){
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
fac[i]=fac[i-1]*i%mod;
}
invf[n]=qpow(fac[n],mod-2);
for(int i=n-1;~i;i--)
invf[i]=invf[i+1]*(i+1)%mod;
fack[0]=1; fack[1]=inv[2];
for(int i=2;i<=n;i++)
fack[i]=fack[i-1]*(i-inv[2]+mod)%mod;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int ans=0;
for(int i=1,j=n;i<=n;i++,j--){
g[i]=a[i]*C(n-1,i-1)%mod*fack[i-1]%mod*fack[j-1]%mod*invf[n-1]%mod;
ans=(ans+g[i])%mod;
}
cout<<ans;
return 0;
}
P10863 [HBCPC2024] Enchanted
给你一个附魔书序列 \(a\),支持以下操作:
- 求合并 \([l,r]\) 所有附魔书可以得到的最高等级(实际上不做任何修改);
- 合并 \([l,r]\) 所有附魔书,再与等级为 \(k\) 的附魔书合并,求附魔花费(实际上不做任何修改);
- 将 \(a_x\) 的等级修改为 \(k\);
- 返回第 \(t\) 次操作之后。
We could merge two books with the same level \(l\) into one new book(the older two will disappear). The level of the new book is \(l+1\) and the merger costs \(2^{l+1}\).
\(a_i\le 30,n,m\le 10^6\)
附魔书的合并很像二进制相加,不妨设附魔书 \(i\) 的权值为 \(2^{a_i-1}\),则合并相当于加法。则原操作为:
- 求区间和的二进制最高位。直接维护区间和即可;
- 求区间和加上 \(k\) 与原来相比的二进制变化位数和。设区间和为 \(t\),则答案为 \(2\times ((t+k)\oplus t\oplus k)\)。
- 其余操作可持久化即可。
时间复杂度 \(O(n\log n)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lc tree[pos].ls
#define rc tree[pos].rs
using namespace std;
const int maxn=1e6+3;
const int mod1=19260817;
const int mod2=1e9+7;
struct T{
int sum,ls,rs;
}tree[maxn<<5];
int tim,cnt;
int n,m,a[maxn],A,P,Q;
int root[maxn];
int rnd(){
return A=(7*A+13)%mod1;
}
void pushup(int pos){
tree[pos].sum=tree[lc].sum+tree[rc].sum;
}
void update(int &pos,int l,int r){
if(!pos) pos=++cnt;
if(l==r){
tree[pos].sum=a[l];
return;
}
int mid=(l+r)>>1;
update(lc,l,mid);
update(rc,mid+1,r);
pushup(pos);
}
void modify(int rt,int &pos,int l,int r,int x,int v){
if(!pos) pos=++cnt; // 新建节点,加 & 保证不会访问到空节点并直接存在儿子中
if(l==r){
tree[pos].sum=v;
return;
}
int mid=(l+r)>>1;
if(x<=mid){
tree[pos].ls=++cnt; // 新建左儿子
tree[pos].rs=tree[rt].rs;
tree[lc]=tree[tree[rt].ls];// 继承右儿子的所有信息
modify(tree[rt].ls,lc,l,mid,x,v);
}else{
tree[pos].rs=++cnt; // 同理
tree[pos].ls=tree[rt].ls;
tree[rc]=tree[tree[rt].rs];
modify(tree[rt].rs,rc,mid+1,r,x,v);
}
pushup(pos);
}
int query(int pos,int l,int r,int L,int R){
if(L<=l&&r<=R) return tree[pos].sum;
int mid=(l+r)>>1,sum=0;
if(L<=mid) sum+=query(lc,l,mid,L,R);
if(mid+1<=R) sum+=query(rc,mid+1,r,L,R);
return sum;
}
int highbit(int x){
int top=0;
while(x){
x/=2;
top++;
}
return top;
}
signed main(){
ios::sync_with_stdio(0);
cin>>n>>m>>A>>P>>Q;
for(int i=1;i<=n;i++){
a[i]=1<<(rnd()%Q);
}
update(root[0],1,n);
for(int i=1,op,l,r,L,R,k;i<=m;i++){
op=rnd()%P+1;
if(op==1){
L=rnd()%n+1;
R=rnd()%n+1;
l=min(L,R);
r=max(L,R);
root[i]=root[i-1];
int t=query(root[i],1,n,l,r);
cout<<highbit(t)%mod2<<'\n';
}else if(op==2){
L=rnd()%n+1;
R=rnd()%n+1;
l=min(L,R);
r=max(L,R);
k=1<<(rnd()%Q);
root[i]=root[i-1];
int t=query(root[i],1,n,l,r);
cout<<((t+k)^t^k)*2%mod2<<'\n';
}else if(op==3){
L=rnd()%n+1;
k=1<<(rnd()%Q);
modify(root[i-1],root[i],1,n,L,k);
}else{
k=rnd()%i;
root[i]=root[k];
}
}
return 0;
}
P11162 「BalkanOI 2023 Day1」Car Race
给你一棵树,有些节点上有车。每个单位时间车向根移动一个单位,当一个根以外的节点同时有多辆车,这些车就爆了无法完成比赛。判断每辆车能否到根,并求所有可到根的车的时间。
\(n\le 10^6\)
对于一个 1 的真子树内,如果存在某个深度有大于一辆车,则它们会爆。考虑 DSU on tree,记 \(s_{dep}\) 表示当前深度为 \(dep\) 的车的编号,先将重儿子的答案计入,再跑轻儿子,每步判断是否合法,不合法直接更新对于节点答案并标记该深度会爆,最后消去轻儿子的影响即可。时间复杂度 \(O(n\log n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+3;
int n,fa[maxn],dep[maxn],idx[maxn],car[maxn],ans[maxn],son[maxn],siz[maxn],dfn[maxn],rdfn[maxn],dfncnt;
vector<int>e[maxn];
void dfs(int u){
dfn[u]=++dfncnt;
idx[dfncnt]=u;
dep[u]=dep[fa[u]]+1;
siz[u]=1;
for(int v:e[u]){
if(v!=fa[u]){
dfs(v);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]) son[u]=v;
}
}
rdfn[u]=dfncnt;
}
int sdep[maxn];
vector<int>br1,br2;
void add(int u){
for(int i=dfn[u];i<=rdfn[u];i++){
int v=idx[i];
if(ans[v]||!car[v]) continue;
if(sdep[dep[v]]==-1) ans[v]=1;
else if(sdep[dep[v]]){
ans[sdep[dep[v]]]=1;
br1.emplace_back(dep[v]);
br2.emplace_back(dep[v]);
ans[v]=1; sdep[dep[v]]=-1;
}else{
sdep[dep[v]]=v;
br1.emplace_back(dep[v]);
}
}
}
void dfs1(int u){
for(int v:e[u]){
if(v!=fa[u]&&v!=son[u]){
dfs1(v);
for(int i:br1) sdep[i]=0;
br1.clear();
}
}
if(son[u]) dfs1(son[u]);
if(u==1) return;
if(car[u])sdep[dep[u]]=u,
br1.emplace_back(dep[u]);
for(int v:e[u]){
if(v!=fa[u]&&v!=son[u]) add(v);
}
for(int i:br2) sdep[i]=0;
br2.clear();
}
signed main(){
cin>>n;
for(int i=2;i<=n;i++){
cin>>fa[i];
fa[i]++;
e[i].emplace_back(fa[i]);
e[fa[i]].emplace_back(i);
}
dfs(1);
for(int i=1;i<=n;i++){
cin>>car[i];
}
dfs1(1);
for(int i=1;i<=n;i++){
if(ans[i]||!car[i]) cout<<-1<<' ';
else cout<<dep[i]-1<<' ';
}
return 0;
}
P11157 【MX-X6-T3】さよならワンダーランド
给你一个序列 \(a\),对于每个 \(i\in[1,n]\),求一个 \(j\) 满足:
- \(1\le i+j\le n\);
- \(a_i\le j\le a_{i+j}\)
若不存在输出 0。
\(n\le 3\times 10^5,|a_i|\le 10^9\)
记 \(k=i+j\),则原式为 \(\exists k\in[1,n],a_i+i\le k\le a_k+i\),拆成 \(a_i+i\le k\) 和 \(i\le a_k-k\),将所有 \(a_i+i\le n\) 的数开一个桶存起来,然后取 \(a_k-k\) 的后缀最大值 \(M\),每次判断桶 \(i\) 里的数 \(t\) 是否满足 \(M\ge-t\) 即可。时间复杂度 \(O(n)\),当然也可以线段树维护。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+3;
int n,a[maxn],ans[maxn];
vector<int>v[maxn];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
ans[i]=-2e9;
if(a[i]+i>n) continue;
v[max(1,a[i]+i)].emplace_back(i);
}
int pos=0,mx=-2e9;
for(int i=n;i;i--){
if(mx<a[i]-i) pos=i,mx=a[i]-i;
for(int j:v[i]){
if(mx+j>=0) ans[j]=pos-j;
}
}
for(int i=1;i<=n;i++){
if(ans[i]==-2e9) cout<<"0\n";
else cout<<"1 "<<ans[i]<<'\n';
}
return 0;
}
P9108 [PA2020] Malowanie płotu
考试原题。同时纪念第一道独立写出来的紫题。
有一个 \(n\) 行 \(m\) 列的木板,对于每一行涂色连续的一段,满足相邻两行涂色部分有相邻,求合法的方案数。
\(n\times m \le 10^7\)
考虑一个显然的 DP,设 \(f(i,L,R)\) 表示第 \(i\) 行涂色 \([L,R]\) 的方案数,则方案数为总方案数减去不交的方案数,则有转移:
显然可以滚掉 \(i\) 一维。
记 \(g(L,R)\) 为 \(f(,1\sim L,1\sim R)\) 的前缀和。则有:
考虑到 \(L\le R\),所以 \(g(m,R)=g(R,R)\),进一步
时间复杂度 \(O(m^2n)\)。貌似优化不了了?上 trick——拆维度。
考虑修改 \(f\) 的定义:设 \(f_L(L,len)\) 为 \([L,L+len-1]\) 的方案,则
将 \(len\) 拿出来枚举
同理有
记 \(u(x)\) 表示 \(g(x,m)\),\(v(x)\) 表示 \(g(m,x)=g(x,x)\),则
再设 \(u'(x)=\sum\limits_{i=1}^x u(i),v'(x)=\sum\limits_{i=1}^x v(i)\),则有
大力转移即可,注意取模,最后答案为 \(u(m)/v(m)\)(这两个值是相等的,若不等就说明写挂了)。时空复杂度皆为 \(O(nm)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
const int maxn=1e7+3;
// const int mod=998244353;
using namespace std;
int n,m,mod;
int f[2][maxn],g[2][maxn],uu[maxn],vv[maxn];
signed main(){
// freopen("true.in","r",stdin);
// freopen("true.out","w",stdout);
cin>>n>>m>>mod;
for(int i=1;i<=m;i++)
f[0][i]=m-i+1,f[1][i]=i;
for(int i=1;i<=m;i++){
g[0][i]=(g[0][i-1]+f[0][i])%mod;
g[1][i]=(g[1][i-1]+f[1][i])%mod;
uu[i]=(uu[i-1]+g[0][i])%mod;
vv[i]=(vv[i-1]+g[1][i])%mod;
}
for(int i=2;i<=n;i++){
for(int L=1;L<=m;L++)
f[0][L]=((-m+L-1)*g[1][L-1]%mod+uu[m]-uu[L-1]+mod+mod)%mod;
for(int R=1;R<=m;R++)
f[1][R]=((R)*g[0][R]-vv[R-1]+mod)%mod;
for(int i=1;i<=m;i++){
g[0][i]=(g[0][i-1]+f[0][i])%mod;
g[1][i]=(g[1][i-1]+f[1][i])%mod;
uu[i]=(uu[i-1]+g[0][i])%mod;
vv[i]=(vv[i-1]+g[1][i])%mod;
}
}
cout<<g[0][m];
return 0;
}
P8590 『JROI-8』这是新历的朝阳,也是旧历的残阳
少女于海边伫立,凝视着落日最后的余晖
“已然过去了呢,旧历的一年......”
给你一个不降序列 \(a\),对于每个 \(m\in [1,k]\),将序列划分成 \(m\) 段,对于第 \(i\) 段将段内的数加上 \(i\),并求序列平方的和。
\(n\le 10^6,k\le 10^7,|a_i|\le 10^7\)
单独计算 \(m=1\) 的答案。对于 \((a_i+1)^2>(a_i+m)^2\) 的数放在第一段,其余放在最后一段一定是最优的。考虑到随着 \(m\) 增大这个分界点逐渐左移,于是一个指针向左走,前缀和统计答案即可。时间复杂度 \(O(n+k)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
const int maxn=1e6+3;
const int mod=998244353;
using namespace std;
int n,k,a[maxn],p1[maxn],p2[maxn],p3[maxn];
signed main(){
cin>>n>>k;
int neg=n;
__int128 ans1=0,ans2=0;
for(int i=1;i<=n;i++){
cin>>a[i];
ans1=(ans1+(a[i]+1)*(a[i]+1)%mod)%mod;
}
if(k==1){
cout<<(long long)ans1;
return 0;
}
for(int i=1;i<=n;i++){
p1[i]=(p1[i-1]+a[i])%mod;
p2[i]=(p2[i-1]+a[i]*a[i]%mod)%mod;
p3[i]=(p3[i-1]+(a[i]+1)*(a[i]+1)%mod)%mod;
}
for(int i=2;i<=k;i++){
while(neg&&abs(a[neg]+1)<abs(a[neg]+i)) neg--;
int d0=n-neg,tx=d0*i%mod*i%mod;
tx=(tx+2*i*(p1[n]-p1[neg]+mod)%mod)%mod;
tx=(tx+p2[n]-p2[neg]+mod)%mod;
tx=(tx+p3[neg])%mod;
ans2=(ans2+tx)%mod;
}
cout<<(long long)((ans1+ans2)%mod);
return 0;
}
P7981 [JRKSJ R3] system
给你一个数列 \(p_i\),每次操作同时 \(p_i=p_{p_i}\),求 \(k\) 次操作后的 \(p\) 数列。
\(p_i\le n\le 5\times 10^5,k\le 10^{18}\)
部分分(50 pts):\(p\) 是一个排列。
即为 ABC377E Permute K times 2,最新最热
考虑建图,连边 \(i\to p_i\)(本质上就是 \(p_i\to p_{p_i}\)),可以形成若干置换环,而每次操作将会 \(p_i\to p_{p_i}\to p_{p_{p_{p_{i}}}}\to p_{p_{p_{p_{p_{p_{p_{p_{i}}}}}}}}\to \cdots\)(\(2^k\) 个 \(p\)),相当于在环上走了 \(2^k\) 步到达的位置,直接上快速幂,对环长取模即可。时间复杂度 \(O(n\log k)\)。
100 pts:
同样建图,会形成内向基环树森林。考虑分两步:先跳到环上,再在环上按 50 分做法快速幂。第一步可以用一个类似求 LCA 时用的 \(2^x\) 级祖先的一个倍增数组 \(fa(x,i)\),由于 \(n\le 5\times 10^5\),\(x\) 最多预处理到 \(19\)。对于 \(k\le 19\),直接跳 \(fa(k,i)\) 即为答案;对于 \(k>19\),直接跳 \(2^{20}-1\) 级祖先,一定能跳到环上,然后再在环上走 \(2^k-(2^{20}-1)\) 步即可。时间复杂度为 \(O(n\log n+n\log k)\)。
代码实现略有不同。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=5e5+7;
int n,k,rcnt;
int fa[20][maxn],vis[maxn],p[maxn],siz[maxn],ans[maxn],scc[maxn];
vector<int>v[maxn];
int qpow(int a,int b,int mod){
int res=1;
for(;b;b>>=1,a=a*a%mod) if(b&1) res=res*a%mod;
return res;
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>fa[0][i]; p[i]=fa[0][i];
}
for(int j=1;j<20;j++)
for(int i=1;i<=n;i++)
fa[j][i]=fa[j-1][fa[j-1][i]]; // 处理倍增数组
for(int i=1;i<=n;i++){ // 找环
if(vis[i]) continue;
int j,l;
for(j=i;!vis[j];j=p[j]) vis[j]=-i;
if(vis[j]>-i) continue; // 不重复找环
v[++rcnt].emplace_back(j);
vis[j]=++siz[rcnt],scc[j]=rcnt;
for(l=p[j];l!=j;l=p[l]) v[rcnt].emplace_back(l),vis[l]=++siz[rcnt],scc[l]=rcnt;
} // vis[i]>0 表示 i 在环上的相对顺序
for(int i=1;i<=n;i++){
if(k<20){ // 直接跳
ans[i]=fa[k][i];
}else{
ans[i]=i;
int step=0;
for(int j=19;~j;j--){ // 跳到环上
ans[i]=fa[j][ans[i]];
step|=(1<<j);
if(vis[ans[i]]>0){ // 到环上了
int rsiz=siz[scc[ans[i]]];
int rk=((vis[ans[i]]-1+qpow(2,k,rsiz)-step%rsiz+rsiz)%rsiz+rsiz)%rsiz;
ans[i]=v[scc[ans[i]]][rk];
break;
}
}
}
}
for(int i=1;i<=n;i++){
cout<<ans[i]<<' ';
}
return 0;
}