「NOIP2022模拟赛 By LZY」
A
显然题目要求对于 \(a\) 的任意一个排列 \(a_{p_1},a_{p_2},…,a_{p_n}\),\(|a_{p_1}-0|+\sum\limits_{i=2}^{n}{|a_{p_i}-a_{p_{i-1}}\ |}\) 的值。
考虑两部分分开考虑。
第一部分 \(|a_{p_1}-0|\),显然对于每一个 \(p_1\) 的贡献是 \(a_{p_1}\),而 \(p_2\sim p_n\) 有 \((n-1)!\) 种排列方式,所以这一部分对最终答案的贡献是 \(\sum\limits_{i=1}^{n}{a_i\times(n-1)!}\);
第二部分,可以枚举所有相邻的点对 \(i,j\),由于其可以在排列的任意 \((n-1)\) 个位置上,剩下的 \((n-2)\) 个点可以任意排列,所以这部分对最终答案的贡献为:\(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}{|a_i-a_j|\times(n-1)\times(n-2)!} = \sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}{|a_i-a_j|\times(n-1)!}\)
最后我们把两部分加起来:
如果就这样的话,代码的复杂度应该是 \(\mathcal{O}(n^2)\) 的,只能拿到 \(40\,pts\),接下来考虑优化。
我们可以先将 \(a\) 数组升序排序,这样我们就把绝对值去掉了:
这个时候用前缀和优化掉 \(j\) 的循环和最前面的求和即可,时间复杂度为 \(\mathcal{O}(n\log n)\)(排序 \(\mathcal{O}(n\log n)\),查询 \(\mathcal{O}(n)\))。
#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 100005
#define ll long long
using namespace std;
int n; int a[maxn]; ll pre=0,ans=0;
ll gcd(ll xx,ll yy){return (yy==0)?xx:gcd(yy,xx%yy);}
int main(){
// freopen("music_19.in","r",stdin); freopen("music_19.out","w",stdout);
scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); sort(a+1,a+1+n);
for(int i=1;i<=n;i++){ans+=(2*(1LL*(i-1)*a[i]-pre)); pre+=a[i];} ans+=pre;
ll div=gcd(ans,n); printf("%lld %lld",ans/div,n/div);
return 0;
}
B
显然,如果要派人驻守城堡 \(u\),从最后面的有传送门回 \(u\) 的城堡一定是最优的。
考虑贪心。对每一个城堡,都在最后一个有传送门回来的城堡贪心地派兵驻守,若攻打下一个城堡的时候人数不够则重复收回价值最低的城堡的士兵直到够了位置,相当于是开始的时候就不在那些城堡驻兵。
最后还要特判士兵的数量不能为负。
#include<iostream>
#include<cstdio>
#include<queue>
#include<algorithm>
#define maxn 5005
#define ll long long
using namespace std;
int n,m,army,x,y,t=1;ll ans=0;
int a[maxn],b[maxn],c[maxn];
int llast[maxn]; pair<int,int> last[maxn];
priority_queue <int,vector<int>,greater<int> > q;
int main(){
// freopen("castle_20.in","r",stdin); freopen("castle_20.out","w",stdout);
scanf("%d%d%d",&n,&m,&army);
for(int i=1;i<=n;i++) scanf("%d%d%d",&a[i],&b[i],&c[i]),llast[i]=i;
for(int i=1;i<=m;i++){scanf("%d%d",&x,&y);llast[y]=max(llast[y],x);}
for(int i=1;i<=n;i++) last[i]=make_pair(llast[i],i); sort(last+1,last+1+n);
for(int i=1;i<=n;i++){
while(army<a[i]&&!q.empty()){int xx=q.top();q.pop();army++;ans-=xx;}
if(army<a[i]&&q.empty()){printf("-1");return 0;}
army+=b[i];
for(;t<=n&&last[t].first==i;t++){army--;ans+=c[last[t].second];q.push(c[last[t].second]);}
}
while(army<0&&!q.empty()){int xx=q.top();q.pop();army++;ans-=xx;}
printf("%lld",ans);
return 0;
}
C
题目里要求的 \(\sum\limits_{i=1}^{n}{\sum\limits_{j=i}^{n}{F(i,j)}} = \sum\limits_{i=1}^{n}{\sum\limits_{j=i}^{n}{\big(\max(i,j)-\min(i,j)\big)}}\),\(\max,\min\) 就是 \(F(i,j)\) 定义里的树上的 \(i\) 节点到 \(j\) 节点的简单路径经过的所有点的点权的最大值和最小值。两部分分开计算,下面给出 \(\max\) 之和的解法,\(\min\) 同理。
假设题目里给的是边权而非点权,那么显然能用 dsu 求解,也就是:按照权值从小到大枚举边,并用并查集维护点是都已经被连通。那么,每当加入一条边 \(i\) 的时候,\(i\) 的两个端点所在的连通块都由比这条边边权小的边相连,故两个连通块中任意选出一个点,这两个点间的简单路径经过的边的边权最大值一定是 \(i\) 提供的。故 \(i\) 对答案的贡献为 \(w_i\times s_x\times s_y\)(\(x,y\) 为边的两端点,\(s\) 存连通块的大小)(因为是树所以一条边两端点一定不会在同一个连通块中)。
然后我们考虑如何把点权转化到边权。在一条简单路径 \(p_1\rightarrow p_2…\rightarrow p_{|p|}\) 上(\(p_i\) 和 \(p_{i+1}\) 间有边直接相连),我们可以把答案 \(\max(w_{p_1},w_{p_2},…,w_{p_{|p|}})\) 看作是 \(\max\Big(\max(w_{p_1},w_{p_2}),\max(w_{p_2},w_{p_3}),…,\max(w_{p_{|p|-1}}\ ,w_{p_{|p|}})\Big)\),这样我们就能把一条边 \((p_i,p_{i+1})\) 的边权看作是两个端点的点权的较大值。
你永远可以相信测评机的速度
#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 1000005
#define ll long long
using namespace std;
inline int read(){
register int num=0,neg=1; register char ch=getchar();
while(!isdigit(ch)&&ch!='-') ch=getchar();
if(ch=='-'){neg=-1;ch=getchar();}
while(isdigit(ch)){num=(num<<3)+(num<<1)+(ch-'0');ch=getchar();}
return num*neg;
}
int n;ll ans=0; int w[maxn]; struct edge{int u,v,mmin,mmax;}a[maxn];
int f[maxn],s[maxn];
int ffind(int xx){if(f[xx]==xx) return xx; else return f[xx]=ffind(f[xx]);}
bool cmp1(edge aa,edge bb){return aa.mmin>bb.mmin;}
bool cmp2(edge aa,edge bb){return aa.mmax<bb.mmax;}
int main(){
// freopen("tree_25.in","r",stdin); freopen("tree_25.out","w",stdout);
// scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&w[i]);
n=read(); for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<n;i++){
// scanf("%d%d",&a[i].u,&a[i].v);
a[i].u=read(); a[i].v=read();
a[i].mmin=min(w[a[i].u],w[a[i].v]);a[i].mmax=max(w[a[i].u],w[a[i].v]);
}
sort(a+1,a+n,cmp1); for(int i=1;i<=n;i++){f[i]=i;s[i]=1;}
for(int i=1;i<=n;i++){
int rx=ffind(a[i].u),ry=ffind(a[i].v);
ans-=(1LL*a[i].mmin*s[rx]*s[ry]);
f[ry]=rx;s[rx]+=s[ry];
}
sort(a+1,a+n,cmp2); for(int i=1;i<=n;i++){f[i]=i;s[i]=1;}
for(int i=1;i<=n;i++){
int rx=ffind(a[i].u),ry=ffind(a[i].v);
ans+=(1LL*a[i].mmax*s[rx]*s[ry]);
f[ry]=rx;s[rx]+=s[ry];
}
printf("%lld",ans);
return 0;
}
D
首先题目可以简化为:一张初始只有 \(s,t\) 两点与边 \(s\rightarrow t\) 的无向图(所有边应该都是无向的,这里为了方便统一用单向箭头连接),每次操作可以选择图中的一条边,加入一个新点并向选择的这条边的两个端点连边。最后问加入 \(n\) 个点后,使得图中最小割为 \(m\) 的本质不同的图的个数有多少。
首先我们发现,开始的时候只能选择边 \(s\rightarrow t\) 这条边加点,就有了 \(s\rightarrow u,u\rightarrow t\) 两条边。之后我们发现图的最小割变成了 \(mincut(s,t)+min\Big(mincut(s,u),mincut(u,t)\Big)\)。而处理 \(mincut(s,u)\) 与 \(mincut(u,t)\) 又能看成原问题的两个子问题求解。这提示我们使用 DP 求解。
令 \(f_{i,j}\) 表示初始只有 \(s,t\) 与边 \(s\rightarrow t\) 的无向图进行 \(i\) 次操作后得到的最小割为 \(j\) 的不同的图的个数,再令 \(g_{i,j}\) 表示只有 \(s,t,u\) 三点和边 \(s\rightarrow u,u\rightarrow t\) 的无向图进行 \((i-1)\) 次操作后得到的最小割为 \(j\) 的不同的图的个数。
转移的话,首先考虑 \(f\rightarrow g\)。考虑对于 \(s\rightarrow u\) 和 \(u\rightarrow t\) 两条边长出来的子图,我们分别进行了 \(k\) 与 \(i-1-k\) 次操作,那么:
知周所众,对于这种形式,我们使用后缀和能够使其达到线性。即假设 \(sf_{i,j},sg_{i,j}\) 为 \(f_{i,j},g_{i,j}\) 的后缀和(第二维),表示进行了 \(i\) 次操作后最小割不小于 \(j\) 的图数。那么转移方程就是:
然后我们考虑 \(g\rightarrow f\) 的转移。我们发现这是一个背包,但是又不完全是,因为在背包中,各个物品间是有顺序之分的,也就是几个价值和体积相同的物品在背包中先选哪个后选哪个是不同的,而本题中却是相同的。我们枚举每一个 \(g_{i,j}\) 与其中选的个数 \(k\),就能可以得出转移方程:
上面的组合数可以解释为:\(g_{i,j}\) 表示一共有 \(g_{i,j}\) 种不同的图,假设第 \(i\) 种用了 \(x_i\) 个,那么总方法数就是 \(x_1+x_2+…+x_{g_{\,i,j}} = k\) 的所有正整数解的组数,共 \(\dbinom{g_{i,j}+k-1}{k}\) 组。
至于时间复杂度的话,推 \(g\) 大概是 \(\mathcal{O}(n^3)\) 的,推 \(f\) 大概是 \(\mathcal{O}(n^4\ln n)\) 的(枚举 \(k\) 好像是个调和级数)。总的来说就是 \(\mathcal{O}(n^4\ln n)\)。
#include<iostream>
#include<cstdio>
#define maxn 55
#define mod 1000000007
#define ll long long
using namespace std;
int n,m;
ll inv[maxn];
ll f[maxn][maxn],sf[maxn][maxn],g[maxn][maxn],sg[maxn][maxn];
int main(){
// freopen("relation_1.in","r",stdin); freopen("relation_1.out","w",stdout);
scanf("%d%d",&n,&m);
inv[1]=1;f[0][1]=sf[0][1]=1; for(int i=2;i<=50;i++) inv[i]=1LL*mod-(mod/i)*inv[mod%i]%mod;
for(int i=1;i<=n;i++){
for(int j=1;j<=n+1;j++)
for(int k=0;k<n;k++) sg[i][j]=(sg[i][j]+(1LL*sf[k][j]*sf[i-1-k][j]%mod))%mod;
for(int j=1;j<=n+1;j++) g[i][j]=(sg[i][j]-sg[i][j+1]+mod)%mod;
for(int j=1;j<=n+1;j++){
for(int p=n+1;p>=1;p--){
for(int q=n+1;q>=1;q--){
ll times=1;
for(int k=1;k*i<=p&&k*j<=q;k++){
times=(times*(g[i][j]+k-1))%mod*inv[k]%mod;
f[p][q]=(f[p][q]+(f[p-k*i][q-k*j]*times)%mod)%mod;
}
}
}
}
for(int j=n+1;j>=1;j--) sf[i][j]=(f[i][j]+sf[i][j+1])%mod;
}
printf("%lld",f[n][m]);
return 0;
}
E
首先,看到「从 \(n\) 个数中选出 \(k\) 个」不难想到本题为背包问题,而且还是 01 背包。
其次,一个数末尾 \(0\) 的个数只取决于其因数中 \(2\) 和 \(5\) 的数量,即若 \(x=2^a\times5^b\times…\),有 \(\operatorname{round}(x)=\min(a,b)\)。
于是我们就可以着手设计状态:我们记 \(f_{i,j,k}\) 表示从前 \(i\) 个数中选择 \(j\) 个数,这些数的乘积中,质因数 \(2\) 的个数为 \(k\) 的所有方案里,乘积中质因数 \(5\) 的个数的最大值。所以我们的最终答案为 \(\max\limits_{i=0}^{t}{\min(i,f_{n,k,i}\,)}\)(其中 \(t\) 表示所有数中质因数 \(2\) 的个数和),理解为枚举选择的数的乘积中质因数 \(2\) 的个数并与质因数 \(5\) 的个数取最小值。
然后设计状态转移的方程,采用 01 背包的思想,枚举 \(i,j\):
- 若不选第 \(i\) 个数,则有 \(f_{i,j,k} = f_{i-1,j,k}\);
- 若选第 \(i\) 个数,则有 \(f_{i,j,k} = f_{i-1,j-1,k-two_i\ }\ \ +five_i\)。
其中 \(two_i,five_i\) 表示 \(a_i\) 的质因数 \(2\) 和 \(5\) 的个数。
如果怕 \(\mathtt{MLE}\) 的话可以用滚动数组优化。
#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 2005
#define ll long long
using namespace std;
int n,k,cur=0,tot=0,ans=0;
int two[maxn],five[maxn];
int f[2][maxn][11500];
ll a[maxn];
int get2(ll xx){int res=0; while(!(xx%2)){res++;xx/=2;} return res;}
int get5(ll xx){int res=0; while(!(xx%5)){res++;xx/=5;} return res;}
int main(){
//freopen("ex_round_3.in","r",stdin); freopen("ex_round_3.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) {scanf("%lld",&a[i]);two[i]=get2(a[i]);five[i]=get5(a[i]);}
memset(f,-0x3f,sizeof(f));
f[0][0][0]=0;
for(int i=1;i<=n;i++){
tot+=two[i]; cur^=1;
for(int j=0;j<=min(i,k);j++){
for(int k=0;k<=tot;k++){
f[cur][j][k]=max(f[cur][j][k],f[cur^1][j][k]);
if(j&&k>=two[i])
f[cur][j][k]=max(f[cur][j][k],f[cur^1][j-1][k-two[i]]+five[i]);
}
}
}
for(int i=0;i<=tot;i++) ans=max(ans,min(i,f[cur][k][i]));
printf("%d",ans);
return 0;
}
F
正难则反,我们可以分析一下还剩 \(k+1\) 张卡片未被弃置时的情况:
假设此时轮到后手选牌弃置时:
- 若此时卡片上的数字有奇有偶,那么后手只要选 与当前所有牌上数字总和奇偶性相同 的牌就能使剩下的牌上数字之和为偶数,此时
后手必胜
。 - 若此时卡片上的所有数字都是偶数,显然后手不管选哪张都能使剩下的牌上数字之和为偶数,此时
后手必胜
。 - 若此时卡片上的所有数字都是奇数:
1.若 \(k\) 为奇数,那么后手不管选哪张都能使剩下的牌上数字之和为奇数,此时先手必胜
;
2.若 \(k\) 为偶数,那么后手不管选哪张,剩下的牌上数字之和都将为偶数,此时后手必胜
。
所以,在这种情况下,若 \(k\) 为奇数且先手能够把所有偶数牌都弃置,那么先手必胜
,否则后手必胜
。
假设此时轮到先手选牌弃置时:
- 若此时卡片上的数字有奇有偶,那么先手只要选 与当前所有牌上数字总和奇偶性不同 的牌就能使剩下的牌上数字之和为奇数,此时
先手必胜
。 - 若此时卡片上的所有数字都是偶数,显然先手不管选哪张剩下的牌上数字之和都将为偶数,此时
后手必胜
。 - 若此时卡片上的所有数字都是奇数:
1.若 \(k\) 为奇数,那么先手不管选哪张都能使剩下的牌上数字之和为奇数,此时先手必胜
;
2.若 \(k\) 为偶数,那么先手不管选哪张,剩下的牌上数字之和都将为偶数,此时后手必胜
。
所以,在这种情况下,若 \(k\) 为偶数且后手能够把所有偶数牌都弃置,或是能将所有奇数牌弃置,那么后手必胜
,否则先手必胜
。
注意到 \(n=k\) 时没有任何人能够操作,所以特判即可。
#include<iostream>
#include<cstdio>
#define maxn 200005
using namespace std;
int n,k; int a[maxn]; int odd,even;
int main(){
// freopen("ex_card_4.in","r",stdin); freopen("ex_card_4.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){scanf("%d",&a[i]); if(a[i]%2) odd++;else even++;}
if(n==k){if(odd%2) printf("L"); else printf("X"); return 0;}
if((n-k)%2) if(((n-k)/2>=odd)||(!(k&1)&&((n-k)/2>=even))) printf("X"); else printf("L");
else if((k&1)&&((n-k)/2>=even)) printf("L"); else printf("X");
return 0;
}
G
既然一个节点能够控制以其为根的子树的叶子节点,我们不妨按照这棵树的 DFS 序对所有叶子节点重新编号,这样每个节点就能改变新序列中一段元素的值(除特殊说明,下面的“节点”“序列”指的都是处理过的叶子节点)。
此时,在第一阶段中购买了一个树上的节点(对应区间 \([l,r]\)),就能在第三阶段中对这段区间进行区间加减操作,那么我们就能想到差分。也就是能将 \(d_l\) 加任意整数 \(x\),\(d_{r+1}\) 减 \(x\)(\(d\) 为差分数组)。那么,目标就是将所有的值转移到 \(d_{m+1}\) 上(\(m\) 为叶子节点个数)。
那么,如果所有点都连通,就能实现上述转移操作,只要求一遍最小生成树即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 2000005
#define ll long long
using namespace std;
int n,x,y,a[maxn]; int le[maxn],ri[maxn];
struct node{int to,nex;}e[maxn]; int head[maxn],t=0;
void add(int au,int av){e[++t].to=av; e[t].nex=head[au]; head[au]=t;}
struct node2{int from,to,id,value;}ed[maxn];int tt=0,tot=0;
void dfs(int u,int fa){
bool isleaf=1;
for(int i=head[u];i;i=e[i].nex){
if(e[i].to==fa) continue;
isleaf=0; dfs(e[i].to,u);
le[u]=(le[u]==0?le[e[i].to]:le[u]); ri[u]=ri[e[i].to];
}
if(isleaf){le[u]=++tt;ri[u]=tt;}
ed[++tot].from=le[u];ed[tot].to=ri[u]+1;ed[tot].id=u;ed[tot].value=a[u];
}
int f[maxn]; int num=0;ll ans=0;bool vis[maxn];
int ffind(int u){if(f[u]==u) return u; else return f[u]=ffind(f[u]);}
bool cmp(node2 aa,node2 bb){return aa.value<bb.value;}
int main(){
// freopen("ex_game_3.in","r",stdin); freopen("ex_game_3.out","w",stdout);
scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<n;i++){scanf("%d%d",&x,&y);add(x,y);add(y,x);}
dfs(1,-1);
tt++; for(int i=1;i<=tt;i++) f[i]=i; sort(ed+1,ed+tot+1,cmp);
int l=1,r=0;
for(;l<=tot;l=r+1){
r=l; while(r+1<=tot&&ed[r+1].value==ed[l].value) r++;
for(int i=l;i<=r;i++){
int r1=ffind(ed[i].from),r2=ffind(ed[i].to);
if(r1!=r2){num++;vis[ed[i].id]=1;}
}
for(int i=l;i<=r;i++){
int r1=ffind(ed[i].from),r2=ffind(ed[i].to);
if(r1!=r2){ans+=ed[i].value;f[r2]=r1;}
}
}
printf("%lld %d\n",ans,num); for(int i=1;i<=n;i++) if(vis[i]) printf("%d ",i);
return 0;
}
/*
3
642265612 766097154 876464106
2 3
1 2
*/
H
处理出前缀数组 \(pre\),题目就变成了求满足 \(\exists k\in [1,n],j - i = k\times(pre_j - pre_i)\) 的子串 \((i,j\,]\) 的个数。
所以我们可以有两种算法:
- 将上面的式子移项得到 \(k\times pre_i - i = k\times pre_j - j\)。令 \(f_x(k) = k\times pre_x - x\)。循环枚举 \(k\),我们求出 \(f_0(k),f_1(k),…,f_n(k)\),对于每个值 \(f_i(k)\),若有 \(num\) 个 \(j\) 满足 \(f_j(k) = f_i(k)\),那么对答案应该有 \(\dbinom{num}{2}\) 的贡献。
- 枚举 \(i\) 和 \(1\) 的个数 \(x\),找到对应的 \(j\) 的范围 \((l,r]\),对答案的贡献就是 \(\left\lfloor\dfrac{r-i}{x}\right\rfloor-\left\lfloor\dfrac{l-i}{x}\right\rfloor\)。
上面的两个算法都是 \(\mathcal{O}(n^2)\) 的,考虑根号分治,设置阈值 \(T\):
- \(1\le k\le T\) 的时候使用上述第一种算法,复杂度 \(\mathcal{O}(nT)\);
- \(T < k\le n\) 的时候由于 \(x = pre_j-pre_i = \dfrac{j-i}{k} < \dfrac{j-i}{T} < \dfrac{n}{T}\) 比较小,所以采用上述第二种算法,复杂度 \(\mathcal{O}(n\frac{n}{T})\)。但是注意 \(\left\lfloor\dfrac{r-i}{x}\right\rfloor-\left\lfloor\dfrac{l-i}{x}\right\rfloor\) 中要减去 \(k \le T\) 的情况。
总复杂度 \(\mathcal{O}(nT + n\frac{n}{T})\),显然 \(T = \sqrt{n}\) 时取最优 \(\mathcal{O}(n\sqrt{n})\)。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define maxn 200005
#define maxx 100000005
using namespace std;
char a[maxn];int n,x;
int pre[maxn];int num[maxx],v[maxn],tot=0;
long long ans=0;
int main(){
// freopen("substring_40.in","r",stdin); freopen("substring_40.out","w",stdout);
scanf("%s",a+1); n=strlen(a+1); int T=sqrt(n);
for(int i=1;i<=n;i++) pre[i]=pre[i-1]+(a[i]-'0');
for(int k=1;k<=T;k++,tot=0){
for(int i=0;i<=n;i++){x=pre[i]*k-i+n; if(!num[x]){num[x]=1;v[++tot]=x;} else num[x]++;}
for(int i=1;i<=tot;i++){ans+=1LL*num[v[i]]*(num[v[i]]-1)/2;num[v[i]]=0;}
}
for(int k=n/T-(T*T==n?1:0);k>=1;k--){
int l=0,r=0;
while(pre[l+1]<k&&l<=n) l++; if(l==n) continue; r=l+1; while(pre[r+1]==k&&r<=n) r++;
for(int i=0;i<n;i++){
if(pre[r]-pre[i]<k){if(r==n) break; l=r; while(pre[r+1]-pre[i]==k&&r<=n) r++;}
if((r-i)/k>T) ans+=(long long)((r-i)/k-max(T,(l-i)/k));
}
}
printf("%lld",ans);
return 0;
}
完结撒花!!!