20260918NOIP模拟赛#13

跑步训练

题面

小 Z 有两个参数:起始速度 \(V\) 和加速度 \(A\)。将小 Z 跑步视为匀加速直线运动。

有 \(n\) 天训练,每天可以选择以下两种训练至多一种:

  1. 提升初始速度的训练,选择可以将 \(V\) 增加 \(v_i\)。

  2. 提升加速度训练,选择可以将 \(A\) 增加 \(a_i\)。

由于加速度训练太累了,两次相邻的加速度训练相距至少为 \(k\)。

根据以下公式,选择合适的训练方案,求小 Z 跑完 \(s\) 米最少需要几秒。

\[t=\frac{\sqrt{v^2+2as}-v}{a} \]

做法

发现时间 \(t\) 很关键,但是直接算很难,如果知道了 \(t\) 再判断是否可能成立会不会更好算呢?因为是最小化时间 \(t\),考虑二分 \(mid\) 表示答案 \(t\) 是否小于等于 \(mid\)。知道了 \(mid\) 之后可以得到一个不等式:

\[\frac{\sqrt{v^2+2as}-v}{a}\leq mid \]

要判断这个不等式是否可能成立,又有根号又是分式,也不好直接算,变形得到:

\[2s \leq a\cdot mid^2+2v\cdot mid\\ \]

发现这个最终式子非常友好,\(s\) 和 \(mid\) 都已知,\(a,v\) 也是有一次可以直接加,dp 一下即可做出。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<iomanip>
#include<cmath>
using namespace std;
typedef long long ll;
typedef long double ld;
const int N=2e5+5;
const ld eps=1e-9;
int T,n,k,V,A,v[N],a[N];
ll s;
ld f[N];
bool check(ld mid){
	f[0]=(ld)(A*mid*mid+2.0*V*mid);
	if(f[0]+2.0*mid*v[n]>2.0*s||fabs(f[0]+2.0*mid*v[n]-2.0*s)<eps)return true;
	ld maxn=f[0]-v[0];
	for(int i=1;i<=n;i++){
		if(i-k>0)maxn=max(maxn,f[i-k]-2.0*mid*v[i-k]);
		f[i]=maxn+(ld)(a[i]*mid*mid)+2.0*mid*v[i-1];
		if(f[i]+2.0*mid*(v[n]-v[i])>2.0*s||fabs(f[i]+2.0*mid*(v[n]-v[i])-2.0*s)<eps)return true;
	}
	return false;
}
void solve(){
	cin>>n>>k>>s>>V>>A;
	for(int i=1;i<=n;i++)cin>>v[i];
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)v[i]+=v[i-1];
	ld l=0,r=s;
	while(r-l>eps){
		ld mid=(l+r)/2;
		if(check(mid))r=mid;
		else l=mid;
	}
	cout<<fixed<<setprecision(10)<<l<<"\n";
	return;
}
int main(){
	freopen("run.in","r",stdin);
	freopen("run.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)solve();
	return 0;
}

必胜赌局

题目描述

有 \(n\) 个人赌球,球赛的结果是 \(A,T,B\) 中的一种,第 \(i\) 个人赌结果是 \(c_i\),如果赌对了,你需要支付给他 \(a_i\) 元,否则你将会获得 \(b_i\) 元。请选择一个最优的赌约集合,只接受其中的赌约,使得最坏情况下你的收益最大。

\(1\leq n,a_i,b_i\leq 500,c_i\in\{A,T,B\}\)

做法

考虑一个赌约集合什么情况是最坏情况,类似反悔贪心,先选择获得所有收益,即获得所有赌约的 \(b\),但这样显然是不行的,赌对了的人并不会给你钱,反而你会损失钱。发现变化就是 \(sum=\sum_{i\in S\wedge c_i=res}(a_i+b_i)\)。找到三种比赛结果中上式最大的就是最坏情况。发现值域很小,那正解大概就是与值域有关的。因为要保证减去的 \(sum\) 是三种比赛结果的最大值,考虑枚举 \(sum\) 并钦定其中一种比赛结果的 \(a+b\) 之和是 \(sum\) 而其他的小于等于 \(sum\),现在固定了 \(sum\),发现 \(b\) 最大,总收益就能最大,考虑设 \(f_{i,c}\) 表示赌约集合中赌球结果为 \(c\) 的赌约的 \(a+b\) 之和等于 \(i\) 时,这个集合的所有 \(b\) 之和的最大值,类似地,设 \(g_{i,c}\) 表示 \(a+b\) 之和小于等于 \(i\) 的最大 \(b\) 之和。转移直接背包 dp,钦定的结果用 \(f\),剩下的结果用 \(g\),答案就是这三个之和和 \(sum\) 的差的最大值。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=505;
int n,ans;
struct node{
	int op,a,b;
}a[N];
int f[3][2*N*N],g[3][2*N*N],sum[3];
char ch;
int main(){
	freopen("shota.in","r",stdin);
	freopen("shota.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	memset(f,-0x3f,sizeof f);
	f[0][0]=f[1][0]=f[2][0]=0;
	for(int i=1;i<=n;i++){
		cin>>ch>>a[i].a>>a[i].b;
		if(ch=='A')a[i].op=0;
		if(ch=='T')a[i].op=1;
		if(ch=='B')a[i].op=2;
		sum[a[i].op]+=a[i].a+a[i].b;
		for(int j=sum[a[i].op];j>=a[i].a+a[i].b;j--)f[a[i].op][j]=max(f[a[i].op][j],f[a[i].op][j-a[i].a-a[i].b]+a[i].b);
	}
	int maxn=max(max(sum[0],sum[1]),sum[2]);
	for(int i=0;i<3;i++){
		g[i][0]=f[i][0];
		for(int j=1;j<=maxn;j++)g[i][j]=max(g[i][j-1],f[i][j]);
	}
	for(int i=0;i<=maxn;i++){
		ans=max(ans,f[0][i]+g[1][i]+g[2][i]-i);
		ans=max(ans,g[0][i]+f[1][i]+g[2][i]-i);
		ans=max(ans,g[0][i]+g[1][i]+f[2][i]-i);
	}
	cout<<ans;
	return 0;
}

接水

题目描述

有 \(m\) 个饮水机,有 \(n\) 种人,每种人有 \(a_i\) 个,这种人的接水时间是 \(t_i\)。

同一个饮水机两个人不能同时接水,求所有人等待时间的和的最小值。

做法

首先按接水时长升序排序,将第 \(i\) 个人分配给第 \((i-1)\mod{m}+1\) 个饮水机,即让每个人逐行填充,上一行安排满了之后这一行从左到右加入人。然后就是模拟加入人类,将一种人分成两类:

  1. 在同一行的,考虑拆贡献,计算这种人对后面的人的贡献,即后面的人数乘这种人的打水时间,后面的人数可以用这种人覆盖的行的人数减去这种人及其前面的人数。

  2. 跨行的,分成三部分统计,前后两个不完整单行按第一类处理,中间一大段完整的行,可以用等差数列求和计算。

发现这两种人的统计都要用到他们在第几行,他们起始和终止在第几列,细心计算就好了。代码中 \(work(l,r,x)\) 计算的是第 \(x\) 行,位于 \(l\sim r\) 饮水机的人的后面等待人数和。\(solve(l,r)\) 表示 \(l\sim r\) 行所有人的等待人数和。

但是为什么这个贪心的构造方案是对的?答案是 \(\sum t_ic_i\),\(t\) 是每个人的接水方案,\(c\)是后面的人的个数。由于每个饮水机中每个人的 \(c=0,1,2\dots k\),\(k\) 是这个饮水机的人数,那么 \(c\) 等于 \(0,1,2\dots\) 的人最多有 \(m\) 个,那么最终局面一定是对于 \(c=0\sim x\) 的人都有 \(m\) 个,对于 \(c=x+1\) 的有 \(<m\) 个人,没有人 \(c>x+1\),因为有更小的肯定选更小的 \(c\)。如果存在 \(t_i<t_j,c_i>c_j\),交换 \(c_i,c_j\) 后会更优,所以最终的结论就是 \(t\) 小的配大的 \(c\),而贪心构造方案刚好能满足这个结论,小的 \(t\) 配大的 \(c\)。但是注意这个构造方案是充分的,但不是必要的。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=3e5+5,M=1e6+5,mod=1e9+7;
int add(int a){return a>=mod?a-mod:a;}
int n,m,fi[N],se[N],s[M];
struct node{
	int a,b;
}a[N];
ll sum,ans;
int work(int l,int r,ll x){
	return add(add(s[r]+mod-s[l-1])+mod-1ll*x*(r-l+1)%mod);
}
int solve(ll l,ll r){
	return add(1ll*s[m]*((r-l+1)%mod)%mod+mod-1ll*(1ll*(l+r)*(r-l+1)/2)%mod*m%mod);
}
bool cmp(node a,node b){
	return a.b<b.b;
}
signed main(){
	freopen("water.in","r",stdin);
	freopen("water.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	fi[1]=1;
	for(int i=1;i<=n;i++)cin>>a[i].a>>a[i].b;
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++){
		se[i]=(fi[i]+a[i].a-2)%m+1;
		fi[i+1]=se[i]%m+1;
		sum+=a[i].a;
	}
	for(int i=1;i<=m;i++){
		s[i]=add(s[i-1]+(sum/m)%mod);
		if(se[n]<m&&i<=se[n])s[i]=add(s[i]+1);
	}
	sum=1;
	for(int i=1;i<=n;i++){
		if(a[i].a<=m&&se[i]>=fi[i]){
			ans=add(ans+1ll*a[i].b*work(fi[i],se[i],sum)%mod);
			if(se[i]==m)sum++;
		}else{
			ans=add(ans+1ll*a[i].b*work(fi[i],m,sum)%mod);
			sum=sum+1;
			a[i].a=a[i].a-(m-fi[i]+1)-se[i];
			if(a[i].a%m!=0)while(1)cout<<"wrong ";
			a[i].a/=m;
			ans=add(ans+1ll*a[i].b*solve(sum,sum+a[i].a-1)%mod);
			sum+=a[i].a;
			ans=add(ans+1ll*a[i].b*work(1,se[i],sum)%mod);
			if(se[i]==m)sum++;
		}
	}
	cout<<ans;
	return 0;
}

传球游戏

题目描述

给出一棵树或一棵带奇环的基环树,你需要对每个点求出以下式子的值。

\[\sum_ival(u,i) \]

定义 \(val(u,v)\) 表示 \(u,v\) 最短路上的点权异或和。

\(3\leq n\leq 3\times 10^5,0\leq a_i 2^30,m\leq n\)

做法

树形 dp+换根 dp+环形 dp。异或和,要么拆位要么 trie 树,先考虑树的情况,如果用 trie 树,那么要知道一个点对的路径点权异或和,可以用两点到根的异或和异或 LCA 处的点权,但这样与 LCA 有关,可以点分治,但是不好写。考虑拆位,对每一位统计每个点只考虑这一位的贡献。这样每个点的点权就变成了 \(0/1\),只用考虑路径上 \(1\) 个数的奇偶性就行了,避免了难处理的异或。那么我们可以设 \(f_{u,0/1}\) 表示将 \(1\) 作为整棵树的根,在 \(u\) 的子树中,和 \(u\) 路径上 \(1\) 的个数是偶/奇数的点的个数。做完这个之后再换根 dp 求出每个点与其他节点路径上有奇数个 \(1\) 的点数,乘上这一位的大小就是对点 \(u\) 的贡献,假设 \(u\) 是整棵树的根,想转移到 \(v\) 是整棵树的根,转移很套路。

树的情况做完了,考虑基环树怎么做。先把不用经过环上的边的路径统计,再处理环。将环复制两遍转成序列,因为要求最短路径的点权异或和,相当于对于某个环上的点,不经过其环上正对面的边到达其他点的路径就是最短路。那么考虑以这个点和它正对面的边将环切成两半,两次 dp 求每个点的左半环的点的子树对这个点子树中的点的贡献,也是拆位。细节看代码。

注意常数大的代码会被卡常,不必要的递归可以写成循环优化。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=3e5+5;
int n,m,a[N],s[N];
ll ans[N];
vector<int>h[N];
int fa[N],siz[N];
int Find(int u){return fa[u]==u?u:fa[u]=Find(fa[u]);}
void Merge(int u,int v){
	if((u=Find(u))==(v=Find(v)))return;
	if(siz[u]>siz[v])swap(u,v);
	fa[u]=v;siz[v]+=siz[u];
	return;
}
void init(int u,int fat){
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i];
		if(v==fat)continue;
		fa[v]=u;
		init(v,u);
	}
	return;
}
bool vis[N];
int f[N][2],g[N][2];
void dfs1(int u,int fat,int X){
	int w=(a[u]>>X)&1;
	f[u][w]=1;f[u][w^1]=0;
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i];
		if(v==fat||vis[v])continue;
		dfs1(v,u,X);
		f[u][0]+=f[v][w];
		f[u][1]+=f[v][1^w];
	}
	return;
}
void dfs2(int u,int fat,int X){
	int W=(a[u]>>X)&1;
	ans[u]+=1ll*f[u][1]*(1ll<<X);
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i],w=(a[v]>>X)&1;
		if(v==fat||vis[v])continue;
		f[0][0]=f[u][0]-f[v][W];
		f[0][1]=f[u][1]-f[v][1^W];
		f[v][0]+=f[0][w];
		f[v][1]+=f[0][1^w];
		dfs2(v,u,X);
	}
	return ;
}
int st[N<<1],tp;
vector<int>son[N];
void Init(int u,int fat,int belong){
	s[u]=(~fat?s[fat]^a[u]:a[u]);
	son[belong].push_back(u);
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i];
		if(v==fat||vis[v])continue;
		Init(v,u,belong);
	}
	return;
}
void subtask2(){
	for(int i=1;i<=n;i++)fa[i]=i,siz[i]=1;
	int u1,u2;
	for(int u=1;u<=n;u++){
		int v;cin>>v;
		if(Find(u)==Find(v))u1=u,u2=v;
		else{
			h[u].push_back(v);
			h[v].push_back(u);
			Merge(u,v);
		}
	}
	fa[1]=0;
	init(1,-1);
	int u=u1,v,sum=0;
	while(u1){
		vis[u1]^=1;
		u1=fa[u1];
		sum++;
	}
	bool flag=0;
	while(u2){
		if(!flag)st[++tp]=u2;
		if(vis[u2]&&!flag)flag=1,v=u2;
		else vis[u2]^=1;
		if(flag)sum--;
		u2=fa[u2];
	}
	for(int i=1;i<=tp/2;i++)swap(st[i],st[tp-i+1]);
	for(int i=tp;i>=1;i--)st[i+sum]=st[i];tp+=sum;sum=0;
	while(u!=v){
		st[++sum]=u;
		u=fa[u];
	}
	for(int i=1;i<=tp;i++)st[i+tp]=st[i];
	for(int i=1;i<=tp;i++)Init(st[i],-1,st[i]);
	for(int i=0;i<30;i++){
		int w=0,v1=0,v0=0;
		for(int j=1;j<=tp;j++){
			dfs1(st[j],-1,i);
			g[st[j]][0]=f[st[j]][0];
			g[st[j]][1]=f[st[j]][1];
			dfs2(st[j],-1,i);
		}
		for(int j=tp/2+2;j<=tp;j++){
			if((a[st[j]]>>i)&1)swap(v1,v0);
			v1+=g[st[j]][1];
			v0+=g[st[j]][0];
			w^=((a[st[j]]>>i)&1);
		}
		for(int j=tp+1;j<=tp*2;j++){
			for(int k=0;k<son[st[j]].size();k++)if((s[son[st[j]][k]]>>i)&1)ans[son[st[j]][k]]+=1ll*v0*(1ll<<i);
			else ans[son[st[j]][k]]+=1ll*v1*(1ll<<i);
			int k=j-tp/2,W=(a[st[j]]>>i)&1;
			w^=((a[st[k]]>>i)&1);
			v1-=g[st[k]][w^1];
			v0-=g[st[k]][w];
			if(W)swap(v1,v0);
			v1+=g[st[j]][1];
			v0+=g[st[j]][0];
			w^=W;
		}
		v1=v0=w=0;
		for(int j=tp/2;j>=1;j--){
			if((a[st[j]]>>i)&1)swap(v1,v0);
			v1+=g[st[j]][1];
			v0+=g[st[j]][0];
			w^=((a[st[j]]>>i)&1);
		}
		for(int j=tp;j>=1;j--){
			for(int k=0;k<son[st[j]].size();k++)if((s[son[st[j]][k]]>>i)&1)ans[son[st[j]][k]]+=1ll*v0*(1ll<<i);
			else ans[son[st[j]][k]]+=1ll*v1*(1ll<<i);
			int k=j+tp/2,W=(a[st[j]]>>i)&1;
			w^=((a[st[k]]>>i)&1);
			v1-=g[st[k]][w^1];
			v0-=g[st[k]][w];
			if(W)swap(v1,v0);
			v1+=g[st[j]][1];
			v0+=g[st[j]][0];
			w^=W;
		}
	}
	return;
}
void subtask1(){
	for(int u=1;u<n;u++){
		int v;cin>>v;
		h[u].push_back(v);
		h[v].push_back(u);
	}
	for(int i=0;i<30;i++){
		dfs1(1,-1,i);
		dfs2(1,-1,i);
	}
	return;
}
int main(){
	freopen("ball.in","r",stdin);
	freopen("ball.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];
	if(m==n)subtask2();
	else subtask1();
	for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
	return 0;
}

看到题目怎么想?决策树

  1. 是否与具体数值大小有关?
    → 拆位。

  2. 是否求最大/最小异或?
    → Trie 或线性基,从高位贪心。

  3. 是否区间异或?
    → 前缀异或 + map / Trie。

  4. 是否树上路径异或?
    → 树形 DP + 换根,或前缀异或 + LCA。

  5. 是否 AND / OR 区间?
    → 拆位 + 双指针 / ST 表。

  6. 是否子集相关?
    → 状压 DP / SOS DP。

  7. 是否范围限制?
    → 数位 DP。

  8. 是否一堆数异或组合?
    → 线性基。

  9. 是否异或最短路?
    → DFS 找环 + 线性基。


位运算题的核心套路:

  • 拆位:把每一位独立处理。
  • 贪心:从高位到低位,配合 Trie / 线性基。
  • DP:树形 DP、换根 DP、数位 DP、状压 DP。
  • 前缀异或:区间异或转成两点异或。
  • Trie / 线性基:处理异或最值和异或空间。
  • 双指针 / ST 表:处理 AND / OR 区间单调性。
  • SOS DP:处理子集/超集的与或计数。
posted @ 2026-09-20 09:30  Tanliu  阅读(16)  评论(0)    收藏  举报