20250722 NOIP模拟赛
20250722 NOIP模拟赛
Problem A. 最大公约数
Description
给定长度为 \(n\) 的序列 \(a\) 和一个正整数 \(k\)。可以进行 \(\leq 1\) 次操作:
- 选择一个区间 \([l,r]\),将 \(a[l,r]\) 的所有元素增加 \(k\)。
最大化 \(\gcd_{i=1}^n a_i\)。\(1\leq n\leq 5\times 10^5,1\leq a_i,k\leq 10^{18}\)。
Solution
序列可以分为三部分:\([1,l),[l,r],(r,n]\)。两边部分对应前后缀 \(\gcd\),而中间的部分则不好处理。
前缀 \(\gcd\) 有良好性质:每次减少时,至少除以 \(2\),那么前缀 \(\gcd\) 只有 \(O(\log V)\) 种取值。
按照这些取值给 \(a\) 分段,发现 \(l-1\) 一定是某一段的结尾,否则一定不优。于是有用的 \(l\) 也只有 \(O(\log V)\) 种。
我们找出这些 \(l\),向后枚举 \(r\),找出答案即可。
看似是 \(O(n\log^2 V)\),其实不然。计算前缀 \(\gcd\) 的过程中,只有 \(\gcd\) 减少时才会花费 \(O(\log V)\) 的复杂度计算,其他时候 \(a_i\) 一定是前缀 \(\gcd\) 的倍数,计算是 \(O(1)\) 的。所以是 \(O(n\log V)\)。
启示:前缀 \(\gcd\) 只有 \(O(\log V)\) 种取值,且计算复杂度为 \(O(n+\log^2 V)\)。
int n;
ll k,a[N],pre[N],suf[N];
void Solve(){
read(n),read(k);
for(int i=1;i<=n;i++){
read(a[i]);
pre[i]=__gcd(pre[i-1],a[i]);
}
ll ans=pre[n];
suf[n+1]=0;
for(int i=n;i;i--) suf[i]=__gcd(suf[i+1],a[i]);
for(int i=1;i<=n;i++){
if(pre[i]==pre[i-1]) continue;
ll v=pre[i-1];
for(int j=i;j<=n;j++){
v=__gcd(v,a[j]+k);
Ckmax(ans,__gcd(v,suf[j+1]));
}
}
printf("%lld\n",ans);
}
signed main(){
int T; read(T);
while(T--) Solve();
return 0;
}
Problem B. 回家路径
Description
给定一张含 \(n\) 点 \(m\) 边的有向图,边有边权 \(w_i\),点有点权 \(a_i\)。
从 \(1\) 开始,初始金钱为 \(p\)。在 \(i\) 点打工一天可以得到 \(a_i\) 金钱,通过边 \(i\) 消耗 \(w_i\) 金钱。问走到 \(n\) 最少打工几天。
\(n\leq 800,m\leq 3000\)。
Solution
按照经典套路,从链开始讨论。
走到 \(i\) 后,决定要不要打工是困难的。于是将决策滞后,当我们金钱不足时,就在已经经过的节点中选择 \(a_j\) 最大的 \(j\) 打工挣钱,这样一定最优。于是我们 \(O(n)\) 扫一遍链,顺便记录一下 \(a_i\) 最大的节点即可。
再接着讨论一般图。从 \(x\) 到 \(y\) 有多条路径,所以我们需要记录 \(a_i\) 最大的节点到底是哪一个。
记 \(f_{i,j}\) 为走到 \(i\) 号点,已经走过的节点中 \(a_j\) 最大,最少打工次数,转移平凡。
如果图是 DAG,拓扑排序+DP 即可。
对于一般的情况,由于转移顺序不好确定,我们跑一个 Dijkstra。
时间复杂度 \(O(nm \log nm)\)。
启示:不好直接决策时,滞后决策;链->(树)->(DAG/环)->图。
int n,m,k;
int head[N],tot;
ll a[N];
bool vis[N][N];
struct Edge{
int to,nxt,val;
}edge[M];
void Add(int u,int v,int w){
edge[++tot]={v,head[u],w};
head[u]=tot;
}
void Clear(){
for(int i=1;i<=n;i++) head[i]=0;
tot=0;
}
struct Node{
ll d,p;
bool operator<(const Node& tmp)const{
return d<tmp.d||(d==tmp.d&&p>tmp.p);
}
bool operator>(const Node& tmp)const{
return d>tmp.d||(d==tmp.d&&p<tmp.p);
}
}dis[N][N];
struct Node2{
int x,y;
Node v;
bool operator<(const Node2& tmp)const{
return v>tmp.v;
}
};
void Dijkstra(){
priority_queue<Node2> q;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]={LINF,-LINF};
memset(vis,0,sizeof(vis));
q.push(Node2{1,1,Node{0,k}});
dis[1][1]={0,k};
while(q.size()){
int x=q.top().x,y=q.top().y;
q.pop();
if(vis[x][y]) continue;
vis[x][y]=1;
for(int i=head[x];i;i=edge[i].nxt){
int t=edge[i].to;
int p=(a[y]>a[t])?y:t;
ll c=max(0ll,(edge[i].val-dis[x][y].p+a[y]-1)/a[y]);
Node res={dis[x][y].d+c,dis[x][y].p+c*a[y]-edge[i].val};
if(dis[t][p]>res){
dis[t][p]=res;
q.push(Node2{t,p,res});
}
}
}
}
void Solve(){
read(n),read(m),read(k);
Clear();
for(int i=1;i<=n;i++) read(a[i]);
for(int i=1;i<=m;i++){
int u,v,w;
read(u),read(v),read(w);
Add(u,v,w);
}
Dijkstra();
Node ans=Node{LINF,-LINF};
for(int i=1;i<=n;i++) Ckmin(ans,dis[n][i]);
if(ans.d>1e18) puts("-1");
else printf("%lld\n",ans.d);
}
signed main(){
int T; read(T);
while(T--) Solve();
return 0;
}
Problem C. 树上划分
Description
给定一棵含 \(n\) 个点的树,每个节点有边权 \(a_i\)。
将树划分为若干个连通块。一个连通块的权值是其内部点权非严格次大值。
求连通块权值的最大值。\(n\leq 5\times 10^5,1\leq a_i\leq 10^9\)。
Solution
先考虑链。划分出的段的两端一定分别是最大值和次大值,否则不优。
进一步,我们可以将一段序列 $a[l\sim r] $ 的权值重新定义为 \(\min(a_l,a_r)\),而不影响答案。
所以设 \(f_i\) 为 \(a[1\sim i]\) 划分出的最大权值和,转移平凡,线段树优化可以做到 \(O(n\log n)\)。
继续考虑树,发现划分出的连通块一定是一条链,链的两端分别是最大值和最小值,且仍然可以将权值重新定义为链的两端权值较小值。
考虑树形 DP。设 \(f_{x,i}\) 为 \(x\) 点子树中还有一个权值为 \(i\) 的点尚未匹配端点时的最大权值和,\(g_x\) 为 \(x\) 子树中所有节点处理完毕的最大权值和。
讨论 \(x,y\) 间的边是否割断,不难列出转移:
\(\sum g_{y'}\) 表示 \(y\) 之前的的所有 \(y'\) 的 \(g\) 之和。
离散化后,直接转移做到 \(O(n\min(n,V))\)。
进一步,这个式子形式非常好,可以线段树合并优化转移。最后做到 \(O(n\log n)\)。
启示:转化权值计算方式;线段树合并优化转移。
int n,a[N],m,c[N];
int head[N],tot;
ll g[N];
struct Edge{
int to,nxt;
}edge[N<<1];
void Add(int u,int v){
edge[++tot]={v,head[u]};
head[u]=tot;
}
struct SegNode{
int lc,rc;
ll add,val1,val2;
}tr[N*25];
int pct,root[N];
void Pushup(int p){
tr[p].val1=max(tr[tr[p].lc].val1,tr[tr[p].rc].val1);
tr[p].val2=max(tr[tr[p].lc].val2,tr[tr[p].rc].val2);
}
void Update(int &p,int l,int r,int x,ll v){
if(!p) p=++pct;
if(l==r){
tr[p].val1=v;
tr[p].val2=v+c[l];
return;
}
int mid=(l+r)>>1;
if(x<=mid) Update(tr[p].lc,l,mid,x,v);
else Update(tr[p].rc,mid+1,r,x,v);
Pushup(p);
}
void WorkAdd(int p,ll v){
tr[p].add+=v;
tr[p].val1+=v;
tr[p].val2+=v;
}
void Spread(int p){
if(!tr[p].add) return;
if(tr[p].lc) WorkAdd(tr[p].lc,tr[p].add);
if(tr[p].rc) WorkAdd(tr[p].rc,tr[p].add);
tr[p].add=0;
}
ll V1,V2,res;
int Merge(int p,int q,int l,int r){
if(!p&&!q) return 0;
if(!p){
Ckmax(V2,tr[q].val2);
Ckmax(res,tr[q].val1+V1);
return q;
}
if(!q){
Ckmax(V1,tr[p].val2);
Ckmax(res,tr[p].val1+V2);
return p;
}
if(l==r){
ll tmp=max({V1+tr[q].val1,V2+tr[p].val1,tr[p].val1+tr[q].val1+c[l]});
Ckmax(res,tmp);
Ckmax(V1,tr[p].val2),Ckmax(V2,tr[q].val2);
Ckmax(tr[p].val1,tr[q].val1);
Ckmax(tr[p].val2,tr[q].val2);
return p;
}
int mid=(l+r)>>1; Spread(p),Spread(q);
tr[p].lc=Merge(tr[p].lc,tr[q].lc,l,mid);
tr[p].rc=Merge(tr[p].rc,tr[q].rc,mid+1,r);
Pushup(p); return p;
}
void dfs(int x,int pr){
Update(root[x],1,m,a[x],0);
ll sum=0;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;
if(y==pr) continue;
dfs(y,x);
WorkAdd(root[x],g[y]);
WorkAdd(root[y],sum);
sum+=g[y]; res=V1=V2=0;
root[x]=Merge(root[x],root[y],1,m);
g[x]=max(g[x]+g[y],res-sum);
}
}
signed main(){
read(n);
for(int i=1;i<=n;i++){
read(a[i]);
c[i]=a[i];
}
sort(c+1,c+n+1);
m=unique(c+1,c+n+1)-c-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(c+1,c+m+1,a[i])-c;
for(int i=1;i<n;i++){
int u,v;
read(u),read(v);
Add(u,v),Add(v,u);
}
dfs(1,0);
printf("%lld\n",g[1]);
return 0;
}
Problem D. 还原排列
Description
https://codeforces.com/contest/1540/problem/D
Solution
对于一个序列 \(b\),可以树状数组上倍增求出 \(p\),复杂度 \(O(n\log n)\)。但加上修改就寄了,变成 \(O(qn\log n)\)。
由于查询是单点值,考虑如何求出单点。设 \(a_i=i-b_i\),即 $p_i $ 在 \(1\sim i\) 中的排名。
初始 \(p_i=a_i\)。从 \(i+1\) 向后枚举 \(j\)。每加入一个 \(j\),值域在 \([a_j,j-1]\) 中的 \(a_k\) 就都要上移。所以若 \(a_i\leq p_j\),则 \(p_j\leftarrow p_j+1\)。这样就是 \(O(qn)\) 了。
考虑线段树维护信息,维护区间的函数 \(f(x)\),即 \(x\) 从这个区间出来后会变成什么。
这个函数的形式很好,是段数不超过 \(len\) 的分段函数,每段斜率都是 \(1\)。 不妨维护 \(f(x)\) 的分界点。
考虑合并区间,也就是进行一个函数复合 \(h(x)=g(f(x))\)。
设 \(f_i,g_j\) 分别为 \(f(x),g(x)\) 的第 \(i,j\) 个分界点。画图+分析,若 \(h\) 的上一个分界点对应前 \(i\) 个 \(f_i\) 与前 \(j\) 个 \(g_j\),那么下一个分界点一定是 \(f_{i+1}\) 和 \(\max(g_{j+1}-i,f_i)\) 中之一。
类似归并,合并做到 \(O(len)\)。建树复杂度 \(O(n)\);查询在 \(O(\log n)\) 个节点上二分,复杂度 \(O(\log^2 n)\);但修改还是 \(O(n)\)。
查询和修改不平衡,那么上分块平衡一下。每 \(B\) 个分一块,块内开线段树。修改 \(O(B)\);查询时散块暴力扫,整块二分,复杂度 \(O(\frac n B \log n)\)。取 \(B=\sqrt{n\log n}\),复杂度 \(O(q\sqrt{n\log n})\)。
int n,Q;
int a[N];
const int B=555;
struct SegTrees{
vector<int> tr[2205];
void Pushup(int p){
int ls=p<<1,rs=p<<1|1;
int cl=tr[ls].size()-1,cr=tr[rs].size()-1;
tr[p].clear(); tr[p].push_back(0);
for(int i=0,j=0;;){
if(i==cl&&j==cr) break;
else if(i==cl) tr[p].push_back(max(tr[rs][j+1]-i,tr[ls][i])),++j;
else if(j==cr) tr[p].push_back(tr[ls][i+1]),++i;
else{
int v1=tr[ls][i+1];
int v2=max(tr[ls][i],tr[rs][j+1]-i);
if(v1<=v2) tr[p].push_back(v1),++i;
else tr[p].push_back(v2),++j;
}
}
}
void Buildtr(int p,int l,int r){
if(l==r){
tr[p].push_back(0);
tr[p].push_back(a[l]);
return;
}
int mid=(l+r)>>1;
Buildtr(p<<1,l,mid);
Buildtr(p<<1|1,mid+1,r);
Pushup(p);
}
void Update(int p,int l,int r,int x){
if(l==r){
tr[p].clear();
tr[p].push_back(0);
tr[p].push_back(a[x]);
return;
}
int mid=(l+r)>>1;
if(x<=mid) Update(p<<1,l,mid,x);
else Update(p<<1|1,mid+1,r,x);
Pushup(p);
}
int Ask(int x){
int p=upper_bound(tr[1].begin(),tr[1].end(),x)-tr[1].begin()-1;
return x+p;
}
}Seg[375];
int L[N],R[N],bl[N],C;
void Update(int x){
int p=bl[x];
Seg[p].Update(1,L[p],R[p],x);
}
int Ask(int x){
int v=a[x],p=bl[x];
for(int i=x+1;i<=R[p];i++)
if(a[i]<=v) v++;
for(int i=p+1;i<=C;i++) v=Seg[i].Ask(v);
return v;
}
signed main(){
read(n);
for(int i=1;i<=n;i++)
read(a[i]),a[i]=i-a[i];
for(int i=1;i<=n;i+=B){
C++; L[C]=i,R[C]=min(n,i+B-1);
for(int j=L[C];j<=R[C];j++) bl[j]=C;
Seg[C].Buildtr(1,L[C],R[C]);
}
read(Q);
while(Q--){
int op; read(op);
if(op==1){
int x,v; read(x),read(v);
a[x]=x-v; Update(x);
}
else{
int x; read(x);
printf("%d\n",Ask(x));
}
}
return 0;
}

浙公网安备 33010602011771号