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\) 间的边是否割断,不难列出转移:

\[f_{x,i}= \max(f_{x,i}+g_y,f_{y,i}+\sum g_{y'}) \]

\[g_x=\max(g_x+g_y,f_{x,i}+f_{y,j}+\min(i,j)) \]

\(\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;
}
posted @ 2025-07-29 17:50  XP3301_Pipi  阅读(18)  评论(0)    收藏  举报
Title