11月5日 NOIP模拟(flandre、meirin、sakuya、scarlet) - 模拟赛总结
Preface
这场所有题都没有漏暴力分或者特殊性质,但是 T1 挂了 \(30\) 分,所以还是那句话:一定,一定,一定要多测几组边界 Hack 数据!
flandre
做得挺久的,大约做了 \(\rm 1h+\)。
首先,选出来的序列一定是升序的,因为交换升序序列中的任意两个都不可能让「感觉效果」更高。
然后来看选那些数组成这个序列。
接下来是我赛时的想法:
如果全为正数,那么自然正数全部都得选。需要考虑的是负数的情况。
首先,选择一个负数不仅会影响到「真实效果」比它大的烟花的「感觉效果」,比它小的也会影响,两边都有后效性,所以无法通过枚举来动态规划或贪心。
设当前已经选了的序列为 \(c_1,c_2,\dots,c_m\) 且已经有序(升序),那么当判断是否要加入一个新的数 \(c_0\) 时,假如选它不能让总「感觉效果」更大,那么「真实效果」小于等于 \(c_0\) 的更不行(可能的额外「感觉效果」一样,「真实效果」却不大于 \(c_0\))。
反之,要让「真实效果」小于等于 \(c_0\) 的烟花也能有有效贡献,\(c_0\) 就必须要选,这样才能补齐这些烟花的额外「感觉效果」来让它们有效。
最后,如果选择的序列中间空了一段,那么把这空的一段补齐贡献一定为正,因为这一空段的贡献一定大于已选的较小的一段的贡献,既然那一段都可以选,这一段当然也可以选。
综上所述,在排序后,所选的序列一定是靠右(「真实效果」较大)且连续的。
所以直接从大到小扫描,处理出如果选了 \(a_i\) 所得到的 \(b_i\)(在 \(a\) 值比自己大的所有烟花都被选了的前提下),然后从右往左找最大前缀和即可。
另外,连续相等的不能互相做出贡献,这个要注意。
(因为变量初始化错了 + 可恶的 Subtask 评测方式炸了 \(30\) 分,恶心!)
点击查看代码 · $100$ 分
#include<cstdio> #include<algorithm> using namespace std; namespace IO{ int read() { int x=0; bool neg=false; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') neg=true; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^'0'); ch=getchar(); } return neg?-x:x; } int sta[55]; void write_u(unsigned int x) { int statop=0; while(x) { sta[++statop]=x%10; x/=10; } while(statop) putchar('0'+sta[statop--]); return; } } const int N=1e6+5; int n; pair<long long,int> a[N]; long long k,b[N],sum[N]; int main() { freopen("flandre.in","r",stdin); freopen("flandre.out","w",stdout); scanf("%d%lld",&n,&k); for(int i=1;i<=n;i++) { a[i].first=IO::read(); a[i].second=i; } sort(a+1,a+n+1); int end=n+1; //应当初始化为n+1而非n!(下同) for(int i=n;i>=1;i--) { if(a[i].first!=a[i+1].first) end=i+1; b[i]=a[i].first+k*(n-end+1); } long long mxsum=0; int mxpos=n+1; for(int i=n;i>=1;i--) { sum[i]=sum[i+1]+b[i]; if(sum[i]>mxsum) { mxsum=sum[i]; mxpos=i; } } printf("%lld %d\n",mxsum,n-mxpos+1); for(int i=mxpos;i<=n;i++) { IO::write_u(a[i].second); putchar(' '); } return 0; }
meirin
赛时最后 \(20\) 分钟把 \(40\) 分所需的公式凑出来了,虽然我也不太会证,但还是把我辛辛苦苦凑出来的式子贴在这里:
设 \(f_i\) 表示以 \(i\) 为右端点时的答案(就是题目公式里的 \(r=i\) 时的和),
答案就是 \(\sum_{i=1}^{n}f_i\),时间复杂度 \(O(QN)\)。
代码(\(40\) 分):
点击查看代码 · $40$ 分
#include<cstdio> #define updmod(x) x=((x)%P+P)%P using namespace std; const int N=5e5+5,P=1000000007; int n,q,a[N],b[N]; long long f[N]; int main() { freopen("meirin.in","r",stdin); freopen("meirin.out","w",stdout); scanf("%d%d",&n,&q); for(int i=1;i<=n;i++) scanf("%d",&a[i]),updmod(a[i]); for(int i=1;i<=n;i++) scanf("%d",&b[i]),updmod(b[i]); for(int i=1;i<=q;i++) { int pl,pr,pk; scanf("%d%d%d",&pl,&pr,&pk); for(int j=pl;j<=pr;j++) b[j]+=pk,updmod(b[j]); long long ans=0,suma=0,sumb=0; for(long long j=1;j<=n;j++) { f[j]=f[j-1]+a[j]*sumb%P+b[j]*suma%P+j*a[j]%P*b[j]%P; updmod(f[j]); suma+=j*a[j],sumb=sumb+j*b[j]; updmod(suma),updmod(sumb); ans+=f[j]; updmod(ans); } printf("%lld\n",ans); } return 0; }
正解是数学推式子,但是待定系数那一步我不太能想得到,后面的其实看起来都蛮好推的。
我的代码:
点击查看代码 · $100$ 分
#include<cstdio> #define updmod(x) x=((x)%P+P)%P using namespace std; const int N=5e5+5,P=1000000007; int n,q,a[N],b[N]; long long prea[N],sufb[N]; long long fl[N],fr[N],f[N],sumf[N]; int main() { freopen("meirin.in","r",stdin); freopen("meirin.out","w",stdout); scanf("%d%d",&n,&q); for(int i=1;i<=n;i++) scanf("%d",&a[i]),updmod(a[i]); for(int i=1;i<=n;i++) scanf("%d",&b[i]),updmod(b[i]); long long ans=0,suma=0,sumb=0,now=0; for(long long i=1;i<=n;i++) { now+=a[i]*sumb%P+b[i]*suma%P+i*a[i]%P*b[i]%P; suma=(suma+i*a[i])%P,sumb=(sumb+i*b[i])%P; ans=(ans+now)%P; } for(int i=1;i<=n;i++) prea[i]=(prea[i-1]+1ll*i*a[i])%P; for(int i=n;i>=1;i--) sufb[i]=(sufb[i+1]+1ll*(n-i+1)*a[i])%P; for(int i=1;i<=n;i++) { fl[i]=((fl[i-1]-prea[i-1])%P+P)%P,fr[i]=(fr[i-1]+sufb[i])%P; f[i]=(fl[i]+fr[i])%P; sumf[i]=(sumf[i-1]+f[i])%P; } for(int i=1;i<=q;i++) { int pl,pr,pk; scanf("%d%d%d",&pl,&pr,&pk); ans+=pk*(sumf[pr]-sumf[pl-1])%P; updmod(ans); printf("%lld\n",(long long)ans); } return 0; }
sakuya
不会,考场上打的暴力,但是题目数据不讲“入德”,明明说暴力有 \(20\) 分的,结果只有 \(15\) 分,哼😕。
要求的当然是所有排列的路径和除以(用乘法逆元)排列的数量,排列的数量确定,是 \(m!\),重点求所有排列的路径和。
最重要的就是一条性质:在 \(a_1,a_2,\dots,a_n\) 的所有排列中,对于某一对下标 \((x,y)\) 在其中连续出现的次数(即形如 \(a_1,\dots,a_x,a_y,\dots,a_n\) 的排列的数量)为 \((m-1)!\),这个结论是我在赛后打表得出的。
那么这样就可以衍生出 \(O(Q N^2)\) 算法:找到每一对有效点(即特殊房间,下同)之间的距离,根据上述结论,这一对有效点出现的次数为 \((m-1)!\),所以这一点对的总贡献就是距离乘以 \((m-1)!\)。将所有的贡献加起来除以 \(m!\) 就是答案。
点击查看代码 · $30$ 分
#include<cstdio> #include<algorithm> using namespace std; const int N=1e6+5,S=N,M=N<<1,P=998244353; int n,s,spec[S],q; struct Allan{ int to,nxt; long long val; }edge[M]; int head[N],idx; inline void add(int x,int y,int z) { edge[++idx]={y,head[x],z}; head[x]=idx; return; } long long quick_pow(long long x,long long y) { long long res=1; while(y) { if(y&1) res=res*x%P; x=x*x%P; y>>=1; } return res; } inline long long inv(long long x) { return quick_pow(x,P-2); } namespace LCA{ const int LogN=20; int fa[N][LogN+5],dep[N]; void DFS(int x) { for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to; if(y==fa[x][0]) continue; fa[y][0]=x,dep[y]=dep[x]+1; DFS(y); } return; } void Init() { DFS(1); for(int k=1;k<=LogN;k++) for(int x=1;x<=n;x++) fa[x][k]=fa[fa[x][k-1]][k-1]; return; } int query(int x,int y) { if(dep[x]<dep[y]) swap(x,y); for(int i=LogN;i>=0;i--) if(dep[fa[x][i]]>=dep[y]) x=fa[x][i]; if(x==y) return x; for(int i=LogN;i>=0;i--) if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i]; return fa[x][0]; } } namespace Subtask_12{ long long fac[N]; void Init_fac() { fac[0]=1; for(int i=1;i<=s;i++) fac[i]=fac[i-1]*i%P; return; } long long dis[N]; void DFS(int x) { for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to,z=edge[i].val; if(y==LCA::fa[x][0]) continue; dis[y]=(dis[x]+z)%P; DFS(y); } return; } inline long long get_dis(int x,int y) { return ((dis[x]+dis[y]-(dis[LCA::query(x,y)]<<1))%P+P)%P; } void Calc() { dis[1]=0; DFS(1); long long ans=0; for(int i=1;i<=s;i++) for(int j=1;j<=s;j++) ans=(ans+get_dis(spec[i],spec[j])*fac[s-1])%P; ans=ans*inv(fac[s])%P; printf("%lld\n",(ans%P+P)%P); return; } inline int rev_edge(int id){return id&1?id+1:id-1;} void Solve() { LCA::Init(); Init_fac(); scanf("%d",&q); for(int p=1;p<=q;p++) { int x,k; scanf("%d%d",&x,&k); for(int i=head[x];i;i=edge[i].nxt) { edge[i].val+=k,edge[rev_edge(i)].val+=k; edge[i].val%=P,edge[rev_edge(i)].val%=P; } Calc(); } return; } } int main() { freopen("sakuya.in","r",stdin); freopen("sakuya.out","w",stdout); scanf("%d%d",&n,&s); for(int i=1;i<n;i++) { int x,y,z; scanf("%d%d%d",&x,&y,&z); add(x,y,z),add(y,x,z); } for(int i=1;i<=s;i++) scanf("%d",&spec[i]); Subtask_12::Solve(); return 0; }
上述总距离可以通过树上换根 DP 求出,具体来说:
设 \(sdis_i\) 表示点 \(i\) 到所有有效点的总距离,\(scnt_i\) 表示点 \(i\) 的子树中有效点的数量,通过一次 DFS 可以求出 \(sdis_1\) 和所有的 \(scnt\)。
然后进行第二次 DFS,设边长为 \(z\),当由父节点 \(x\) 走到子节点 \(y\) 的过程中,\(x\) 上方的有效点(共有 \(scnt_1-scnt_y\) 个)距离都增加了 \(k\),\(y\) 下面的特殊点(共有 \(scnt_y\) 个)的距离都减少了 \(k\),即 \(sdis_y = sdis_x - k \times scnt_y + k \times (scnt_1-scnt_y)\)。
这样就可以求出每个点到所有有效点的距离和了,将所有有效点的 \(scnt\) 加起来就是所有有效点对的距离和,乘以 \((m-1)!\) 除以 \(m!\) 即可。
每次查询都暴力修改涉及到的边,然后每次都用上述方法做一遍。时间复杂度 \(O(Q \times (N+M))\)。
这是代码的核心部分,与上面的代码仅有这一部分不同:
点击查看代码 · $50$ 分
long long sdis[N]; int scnt[N]; void DFS1(int x,int fa=0,long long dep=0) { //is_spec代表是否是有效点 if(is_spec[x]) sdis[1]=(sdis[1]+dep)%P,scnt[x]=1; else scnt[x]=0; for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to; long long z=edge[i].val; if(y==fa) continue; DFS1(y,x,(dep+z)%P); scnt[x]+=scnt[y]; } return; } void DFS2(int x,int fa=0) { for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to; long long z=edge[i].val; if(y==fa) continue; sdis[y]=((sdis[x]-z*scnt[y]%P+z*(scnt[1]-scnt[y])%P)%P+P)%P; DFS2(y,x); } return; } void Calc() { sdis[1]=0; DFS1(1); DFS2(1); long long ans=0; for(int i=1;i<=s;i++) ans=(ans+sdis[spec[i]])%P; ans=ans*fac[s-1]%P*inv(fac[s])%P; printf("%lld\n",ans); return; }
对于每一次修改,考虑某一条边 \(x \leftrightarrow y\),对这条边加上 \(k\) 相当于对覆盖了这条边的每一条(连接有效点的)路径都加上了 \(k\),而这条边连接的其中一个连通块中的每一个有效点都可以和另一个连通块中的每一个有效点组成一条有效路径,所以两边点数的乘积就是路径的数量。
某一连通块的点数可以通过刚才的 \(tcnt\) 来转化,令 \(x\) 为父节点,\(y\) 为子节点,那么路径的数量 \(path_{x,y}\) 就是 \(tcnt_y \times (tcnt_1-tcnt_y)\)。每次将答案加上路径数量乘以 \(k\) 即可。
为了防止被菊花图之类的卡,建议先预处理从每一个点出发的所有边的 \(path\) 值之和。
代码已重构,会简洁一点:
点击查看代码 · $100$ 分
#include<cstdio> #include<algorithm> using namespace std; const int N=1e6+5,S=N,M=N<<1,P=998244353; int n,s,spec[S],q; bool is_spec[N]; struct Allan{ int to,nxt; long long val; }edge[M]; int head[N],idx; inline void add(int x,int y,int z) { edge[++idx]={y,head[x],z}; head[x]=idx; return; } long long quick_pow(long long x,long long y) { long long res=1; while(y) { if(y&1) res=res*x%P; x=x*x%P; y>>=1; } return res; } inline long long inv(long long x){return quick_pow(x,P-2);} long long fac[N]; void Init_fac() { fac[0]=1; for(int i=1;i<=s;i++) fac[i]=fac[i-1]*i%P; return; } long long sdis[N]; int scnt[N],fa[N]; void DFS1(int x,long long dep=0) { if(is_spec[x]) sdis[1]=(sdis[1]+dep)%P,scnt[x]=1; else scnt[x]=0; for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to; long long z=edge[i].val; if(y==fa[x]) continue; fa[y]=x; DFS1(y,(dep+z)%P); scnt[x]+=scnt[y]; } return; } void DFS2(int x) { for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to; long long z=edge[i].val; if(y==fa[x]) continue; sdis[y]=((sdis[x]-z*scnt[y]%P+z*(scnt[1]-scnt[y])%P)%P+P)%P; DFS2(y); } return; } long long tms[N]; int main() { freopen("sakuya.in","r",stdin); freopen("sakuya.out","w",stdout); scanf("%d%d",&n,&s); for(int i=1;i<n;i++) { int x,y,z; scanf("%d%d%d",&x,&y,&z); add(x,y,z),add(y,x,z); } for(int i=1;i<=s;i++) { scanf("%d",&spec[i]); is_spec[spec[i]]=true; } long long ans=0; DFS1(1); DFS2(1); for(int i=1;i<=s;i++) ans=(ans+sdis[spec[i]])%P; Init_fac(); long long mul=fac[s-1]*inv(fac[s])%P; for(int x=1;x<=n;x++) { for(int i=head[x];i;i=edge[i].nxt) { int y=edge[i].to; if(y==fa[x]) tms[x]+=1ll*scnt[x]*(scnt[1]-scnt[x])%P; else tms[x]+=1ll*scnt[y]*(scnt[1]-scnt[y])%P; tms[x]%=P; } } scanf("%d",&q); for(int p=1;p<=q;p++) { int x,k; scanf("%d%d",&x,&k); ans=(ans+k*tms[x]*2)%P; printf("%lld\n",ans*mul%P); } return 0; }
scarlet
首先,题目中所修改的范围有很明显的周期性,具体来说,是区间 \([kx+1,kx+1+y]\),其中 \(k \le \left\lfloor \frac{n-1}{x} \right\rfloor\)。
所以我用了区修区查线段树,每次按照周期修改区间,期望得分 \(40\) 分,实际上可以卡 \(65\)。
点击查看代码 · $65$ 分
#include<cstdio> using namespace std; namespace IO{ template<typename T=int> T read() { T x=0; bool neg=false; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') neg=true; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^'0'); ch=getchar(); } return neg?-x:x; } template<typename T> void write(T x) { if(x<0) { putchar('-'); x=-x; } static int sta[55]; int statop=0; while(x) { sta[++statop]=x%10; x/=10; } while(statop) putchar('0'+sta[statop--]); return; } } const int N=2e5+5; int n,m; long long a[N]; struct SegmentTree{ int l,r; long long lazy,dat; inline int get_mid(){return l+r>>1;} inline int get_len(){return r-l+1;} }tree[N<<2]; #define update(x) tree[x].dat=tree[x<<1].dat+tree[x<<1|1].dat inline void spread(int p) { if(tree[p].lazy) { tree[p<<1].dat+=tree[p].lazy*tree[p<<1].get_len(); tree[p<<1|1].dat+=tree[p].lazy*tree[p<<1|1].get_len(); tree[p<<1].lazy+=tree[p].lazy; tree[p<<1|1].lazy+=tree[p].lazy; tree[p].lazy=0; } return; } void Build(int l,int r,int p=1) { tree[p].l=l,tree[p].r=r; if(l==r) { tree[p].dat=a[l]; return; } int mid=tree[p].get_mid(); Build(l,mid,p<<1),Build(mid+1,r,p<<1|1); update(p); return; } void add(int l,int r,int k,int p=1) { if(l<=tree[p].l&&tree[p].r<=r) { tree[p].dat+=1ll*k*tree[p].get_len(); tree[p].lazy+=k; return; } spread(p); int mid=tree[p].get_mid(); if(l<=mid) add(l,r,k,p<<1); if(r>mid) add(l,r,k,p<<1|1); update(p); return; } long long query(int l,int r,int p=1) { if(l<=tree[p].l&&tree[p].r<=r) return tree[p].dat; spread(p); int mid=tree[p].get_mid(); long long res=0; if(l<=mid) res+=query(l,r,p<<1); if(r>mid) res+=query(l,r,p<<1|1); return res; } #undef update int main() { freopen("scarlet.in","r",stdin); freopen("scarlet.out","w",stdout); n=IO::read(),m=IO::read(); for(int i=1;i<=n;i++) a[i]=IO::read(); Build(1,n); for(int i=1;i<=m;i++) { int op=IO::read(); if(op==1) { int x=IO::read(),y=IO::read(),k=IO::read(); if(y>=x) y=x-1; for(int j=1;j<=n;j+=x) add(j,j+y,k); } if(op==2) { int l=IO::read(),r=IO::read(); long long res=query(l,r); IO::write(res); putchar('\n'); } } return 0; }
果对于 \(x\) 较小的可以使用循环节计算,即计算每一种循环节内长度的前缀和,然后查询时遍历所有循环节找,分别计算完整的循环节和散的循环节。
用这个配合刚才的线段树,可以卡到 \(95\) 分。
点击查看代码 · $95$ 分
#include<cstdio> #include<cmath> using namespace std; namespace IO{ template<typename T=int> T read() { T x=0; bool neg=false; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') neg=true; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^'0'); ch=getchar(); } return neg?-x:x; } template<typename T> void write(T x) { if(!x) { putchar('0'); return; } if(x<0) { putchar('-'); x=-x; } static int sta[55]; int statop=0; while(x) { sta[++statop]=x%10; x/=10; } while(statop) putchar('0'+sta[statop--]); return; } } const int N=2e5+5; int n,m; long long a[N]; struct SegmentTree{ int l,r; long long lazy,dat; inline int get_mid(){return l+r>>1;} inline int get_len(){return r-l+1;} }tree[N<<2]; #define update(x) tree[x].dat=tree[x<<1].dat+tree[x<<1|1].dat inline void spread(int p) { if(tree[p].lazy) { tree[p<<1].dat+=tree[p].lazy*tree[p<<1].get_len(); tree[p<<1|1].dat+=tree[p].lazy*tree[p<<1|1].get_len(); tree[p<<1].lazy+=tree[p].lazy; tree[p<<1|1].lazy+=tree[p].lazy; tree[p].lazy=0; } return; } void Build(int l,int r,int p=1) { tree[p].l=l,tree[p].r=r; if(l==r) { tree[p].dat=a[l]; return; } int mid=tree[p].get_mid(); Build(l,mid,p<<1),Build(mid+1,r,p<<1|1); update(p); return; } void add(int l,int r,int k,int p=1) { if(l<=tree[p].l&&tree[p].r<=r) { tree[p].dat+=1ll*k*tree[p].get_len(); tree[p].lazy+=k; return; } spread(p); int mid=tree[p].get_mid(); if(l<=mid) add(l,r,k,p<<1); if(r>mid) add(l,r,k,p<<1|1); update(p); return; } long long query(int l,int r,int p=1) { if(l<=tree[p].l&&tree[p].r<=r) return tree[p].dat; spread(p); int mid=tree[p].get_mid(); long long res=0; if(l<=mid) res+=query(l,r,p<<1); if(r>mid) res+=query(l,r,p<<1|1); return res; } #undef update const int Ns4=1000; long long raw_loop[Ns4+5][Ns4+5],loop[Ns4+5][Ns4+5]; int main() { #ifndef JC_LOCAL freopen("scarlet.in","r",stdin); freopen("scarlet.out","w",stdout); #endif n=IO::read(),m=IO::read(); for(int i=1;i<=n;i++) a[i]=IO::read(); Build(1,n); for(int i=1;i<=m;i++) { int op=IO::read(); if(op==1) { int x=IO::read(),y=IO::read(),k=IO::read(); if(y>=x) y=x-1; if(x<=Ns4) { for(int j=0;j<=y;j++) raw_loop[x][j]+=k; for(int j=0;j<=x;j++) loop[x][j]=(j?loop[x][j-1]:0)+raw_loop[x][j]; } else { for(int j=1;j<=n;j+=x) add(j,j+y,k); } } if(op==2) { int l=IO::read(),r=IO::read(); l--,r--; long long ans=0; for(int j=1;j<=Ns4;j++) { int num=(r/j)-(l/j); ans+=num*loop[j][j-1]; ans-=loop[j][l%j-1]; ans+=loop[j][r%j]; } IO::write(ans+query(l+1,r+1)); putchar('\n'); } } return 0; }
本文采用 「CC-BY-NC 4.0」 创作共享协议,转载请注明作者及出处,禁止商业使用。
作者:Jerrycyx,原文链接:https://www.cnblogs.com/jerrycyx/p/18528688
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步