2023.9.10模拟赛
教师节快乐
回归正题,模拟赛链接
T1
给定长度为 \(n\) 的序列 \(a\),定义区间 \([l,r]\) 的权值为 \(a_l\times a_r\)。\(q\) 次询问给定 \(l\) 和 \(r\),求所有左端点 \(i\) 和右端点 \(j\) 满足 \(l\le i\le j\le r\) 的区间的权值和。
前缀和解决即可,维护 \(a_i\) 的前缀和 \(sum1\) 与 \(a_i\times sum1_{i-1}\) 的前缀和 \(sum2\),答案为
维护的方式有很多种,但是我用的线段树。
因此这里放上我考场上的线段树代码
#include<iostream>
using namespace std;
const int N=1e5+10,M=4e5+10;
typedef long long ll;
int n,m,a[N];
ll sum[N];
struct Segment_Tree
{
ll sum,mul;
#define ls u<<1
#define rs u<<1|1
#define mid (l+r>>1)
}t[M];
void pushup(int u,int l,int r)
{
t[u].mul=t[ls].mul+t[rs].mul+t[ls].sum*t[rs].sum;
t[u].sum=t[ls].sum+t[rs].sum;
}
void build(int u,int l,int r)
{
if(l==r)return t[u].sum=a[l],void();
build(ls,l,mid),build(rs,mid+1,r);
pushup(u,l,r);
}
Segment_Tree query(int u,int l,int r,int pl,int pr)
{
if(pl<=l&&r<=pr)return t[u];
if(pl<=mid&&pr>mid)
{
auto t1=query(ls,l,mid,pl,pr),t2=query(rs,mid+1,r,pl,pr);
return {t1.sum+t2.sum,t1.mul+t2.mul+t1.sum*t2.sum};
}
if(pl<=mid)return query(ls,l,mid,pl,pr);
if(pl>mid)return query(rs,mid+1,r,pl,pr);
}
int main()
{
freopen("summation.in","r",stdin);
freopen("summation.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i]*a[i];
}
build(1,1,n);
while(m--)
{
int l,r;
cin>>l>>r;
auto t=query(1,1,n,l,r);
cout<<t.mul+sum[r]-sum[l-1]<<"\n";
}
return 0;
}
T2
给定正整数 \(n\),非负整数 \(k\) 和一个数 \(m\),你每次可以进行两种操作之一:
- 让 \(n\) 变为 \(n+1\)
- 让 \(n\) 变为 \(2n+k\)
求令 \(n=m\) 最少次数。
之前做过一道类似的题。当时那道题用搜索的方式喜提 TLE,实际上加上记忆化搜索和贪心技巧就能过。这题是那道题的弱化版。
正着推感觉不太优,就可以考虑倒着推。
实际上,可以直接倒着考虑让 \(m\) 变小直到 \(m=n\),即将两个操作变换成:
- 让 \(m\) 变为 \(m-1\)
- 让 \(m\) 变为 \((m-k)/2\),前提是 \(m-k\) 是偶数。
转换后可以直接贪心做,如果当前能进行操作二就进行操作二,否则只能进行操作一。
代码就很简单了
#include<iostream>
using namespace std;
int n,m,k;
int main()
{
freopen("optimality.in","r",stdin);
freopen("optimality.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int T;
cin>>T;
while(T--)
{
cin>>n>>k>>m;
int res=0;
while(n!=m)
if((m-k)%2==0)
if((m-k)/2>=n)m=(m-k)/2,res++;
else
{
res+=m-n;
break;
}
else if(m>n)m--,res++;
cout<<res<<'\n';
}
return 0;
}
T3
给定一个 \(n\) 个点,\(n-1\) 条边,每条边的边权唯一。你需要给边定向,保证所有路径的权值单调下降,输出合法方案的数量。
一个性质:对于连接节点 \(u\) 的所有边 \(e_u\),给其中一条边 \(e_{u,1}\) 定向为指向节点 \(u\)。设其边权为 \(w_{u,1}\),那么对于节点 \(u\) 的所有比 \(w_{u,1}\) 大的边 \(e_{u,i}\),一定也指向当前节点 \(u\)。
有了这个性质,我们就可以考虑考虑树形 DP,遍历整个图。
具体来说,从根节点开始,先给每个节点的所有出边进行排序。我们简称父亲到当前点的边为父亲边,这样就可以将节点分为两类:
- 边权比父亲到当前点小的所有边,我们称其为小边。
- 边权比父亲到当前点大的所有边,我们称其为大边。
对于小边,可以一一定向统计方案,对于大边则需要被动的根据父亲边和小边的结果决定方案数。具体而言:
- 如果某个小边指向父亲的方向,那么所有比它大的边都需要指向父亲方向,同时父亲边需要指向儿子方向。
- 如果所有的小边都指向儿子方向,父亲边指向儿子方向,那么所有大边都只能指向父亲方向。
- 如果所有小边都指向儿子方向,父亲边指向父亲方向,那么如果某个大边指向父亲,则所有比它大的边都需要指向父亲。
我们设状态 \(f_{u,0/1}\) 表示当前父亲边 \(e_u\) 指向儿子/父亲的方案数。
在每次转移的过程中,从小到大枚举哪一条边是第一个指向父亲的边统计答案即可,在维护的过程中,可以用前缀积优化。
代码在这里
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=3e5+10,mod=998244353;
typedef long long ll;
int n,f[N][2];//f[u][0]表示向子节点指,f[u][1]表示向父节点指
typedef pair<int,int>PII;
#define x first
#define y second
vector<PII>edge[N];
int pow(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=(ll)res*a%mod;
a=(ll)a*a%mod;
b>>=1;
}
return res;
}
void dfs(int u,int fa)
{
int din=1,dout=1,now=0;
sort(edge[u].begin(),edge[u].end());
for(PII t:edge[u])
{
int j=t.y;
if(j==fa)continue;
dfs(j,u);
din=(ll)f[j][1]*din%mod;
}
f[u][0]=din,f[u][1]=0;
for(PII t:edge[u])
{
int j=t.y;
if(j==fa)
{
now=1;
f[u][now]=(f[u][now]+(ll)dout*din%mod)%mod;
continue;
}
dout=(ll)dout*f[j][0]%mod;
din=(ll)din*pow(f[j][1],mod-2)%mod;
f[u][now]=(f[u][now]+(ll)dout*din%mod)%mod;
}
}
int main()
{
freopen("direction.in","r",stdin);
freopen("direction.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1,a,b;i<n;i++)
{
cin>>a>>b;
edge[a].push_back({i,b});
edge[b].push_back({i,a});
}
dfs(1,0);
cout<<f[1][0];
return 0;
}
T4
给定一个长度为 \(n\) 的序列 \(a\),保证 \(0\le a_i\le 2\)。\(q\) 次修改,每次形如将在区间 \([l,r]\) 内的所有 \(a_i\) 变为 \((a_i+1)\mod 3\)。每次修改后询问多少区间 \([L,R]\) 同时包含三种元素。
双指针能拿 \(60\) 分,但是正解是线段树。
双指针用前缀和优化,每次维护从当前点开始到哪个点满足条件,最后 \(O(n)\) 统计答案即可。
代码放在这里
#include<iostream>
#include<cstring>
using namespace std;
const int N=5e5+10,inf=0x3f3f3f3f;
typedef long long ll;
int n,m,a[N],cnt[3],ck[N];
bool flag[3];
int main()
{
freopen("maintaining.in","r",stdin);
freopen("maintaining.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
int l=1,r=0;
ll res=0;
memset(ck,0x3f,sizeof ck);
while(1)
{
while((!cnt[0]||!cnt[1]||!cnt[2])&&r+1<=n)cnt[a[++r]]++;
while(cnt[0]&&cnt[1]&&cnt[2])
{
cnt[a[l++]]--;
ck[l-1]=r;
}
if(r==n)break;
}
for(int i=1;i<=n;i++)
if(ck[i]!=inf)res+=n-ck[i]+1;
cout<<res<<'\n';
while(m--)
{
cin>>l>>r;
for(int i=l;i<=r;i++)a[i]=(a[i]+1)%3;
l=1,r=0;
cnt[0]=cnt[1]=cnt[2]=res=0;
memset(ck,0x3f,sizeof ck);
while(1)
{
while((!cnt[0]||!cnt[1]||!cnt[2])&&r+1<=n)cnt[a[++r]]++;
while(cnt[0]&&cnt[1]&&cnt[2])
{
cnt[a[l++]]--;
ck[l-1]=r;
}
if(r==n)break;
}
for(int i=1;i<=n;i++)
if(ck[i]!=inf)res+=n-ck[i]+1;
cout<<res<<'\n';
}
return 0;
}
线段树维护的信息有:每个区间最左边和最右边的 \(0,1,2\) 的位置,最长的相同数字前缀和相同数字后缀,区间的方案数。
代码在这里
#include<iostream>
#include<algorithm>
using namespace std;
const int N=5e5+10,M=N<<2;
typedef long long ll;
int n,m,a[N];
struct Segment_Tree
{
#define ls u<<1
#define rs u<<1|1
#define mid (l+r>>1)
int tag,pre[3],suf[3],tl[3],tr[3];
ll sum;
}t[M];
ll get(int sum)
{
return(ll)sum*(sum+1)/2;
}
void pushup(int u,int l,int r)
{
t[u].sum=t[ls].sum+t[rs].sum;
for(int i=0;i<3;i++)
{
t[u].sum+=get(t[ls].suf[i]+t[rs].pre[i])-get(t[ls].suf[i])-get(t[rs].pre[i]);
t[u].pre[i]=t[ls].pre[i]==mid-l+1?mid-l+1+t[rs].pre[i]:t[ls].pre[i];
t[u].suf[i]=t[rs].suf[i]==r-mid?r-mid+t[ls].suf[i]:t[rs].suf[i];
if(!t[ls].tl[i]||!t[rs].tl[i])
{
t[u].tl[i]=t[ls].tl[i]+t[rs].tl[i];
t[u].tr[i]=t[ls].tr[i]+t[rs].tr[i];
}
else
{
t[u].sum-=get(t[rs].tl[i]-t[ls].tr[i]-1);
t[u].tl[i]=t[ls].tl[i];
t[u].tr[i]=t[rs].tr[i];
}
}
}
void add_tag(int u,int tag)
{
tag%=3;
if(tag)
{
t[u].tag=(t[u].tag+tag)%3;
rotate(t[u].tl,t[u].tl+3-tag,t[u].tl+3);
rotate(t[u].tr,t[u].tr+3-tag,t[u].tr+3);
rotate(t[u].pre,t[u].pre+3-tag,t[u].pre+3);
rotate(t[u].suf,t[u].suf+3-tag,t[u].suf+3);
}
}
void pushdown(int u,int l,int r)
{
if(t[u].tag)
{
add_tag(ls,t[u].tag);
add_tag(rs,t[u].tag);
t[u].tag=0;
}
}
void build(int u,int l,int r)
{
if(l==r)
{
t[u].tl[a[l]]=t[u].tr[a[r]]=l;
t[u].pre[a[l]]=t[u].suf[a[l]]=t[u].sum=1;
return;
}
build(ls,l,mid),build(rs,mid+1,r);
pushup(u,l,r);
}
void update(int u,int l,int r,int pl,int pr)
{
if(pl<=l&&r<=pr)
{
add_tag(u,1);
return;
}
pushdown(u,l,r);
if(pl<=mid)update(ls,l,mid,pl,pr);
if(pr>mid)update(rs,mid+1,r,pl,pr);
pushup(u,l,r);
}
ll query(int u,int l,int r,int pl,int pr)
{
ll res=get(pr-pl+1);
res+=t[1].sum;
for(int i=0;i<3;i++)
if(!t[1].tl[i])res-=get(pr-pl+1);
else res-=get(t[1].tl[i]-1)+get(n-t[1].tr[i]);
return res;
}
int main()
{
freopen("maintaining.in","r",stdin);
freopen("maintaining.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
build(1,1,n);
cout<<query(1,1,n,1,n)<<'\n';
while(m--)
{
int l,r;
cin>>l>>r;
update(1,1,n,l,r);
cout<<query(1,1,n,1,n)<<"\n";
}
return 0;
}