NOIP2025 题解

虽迟但到。

T1.糖果店

\(s_i=x_i+y_i\),设第 \(i\) 种糖果买了 \(r_i\) 个,记 \(r_i=2p_i+q_i\)\(0\le q_i\le 1\)),则总价为 \(p_is_i+q_ix_i\),则显然只有 \(s_i\) 最小的 \(p_i\ne 0\),记为 \(s_j\);也显然只有最小的若干个 \(x_i\)\(q_i=1\),于是把 \(x\) 排序,枚举一段前缀的 \(q_i=1\),剩下的钱全都买 \(s_j\) 即可。

点击查看代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e5+5;
int n,a[N],b[N];
LL m,ans;
signed main(){
//	freopen("candy.in","r",stdin);
//	freopen("candy.out","w",stdout);
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	int mn=INT_MAX;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i];
		mn=min(mn,a[i]+b[i]);
	}
	sort(a+1,a+n+1);
	LL sum=0;
	for(int i=0;i<=n;i++){
		sum+=a[i];
		if(sum>m) break;
		LL tmp=m-sum;
		ans=max(ans,2ll*(tmp/mn)+i);		
	}
	cout<<ans<<'\n';
	return 0;
}

T2.清仓甩卖

万恶之源,赛时想法,可能有些地方想复杂了。
\((a_i,w_i)\) 表示原价为 \(a_i\) 现价为 \(w_i\) 的物品。
考虑求不合法的情况。如果允许买半颗糖果,那么题目给的贪心策略完全没有问题,于是不难想到不合法的情况:我在最后剩下 \(1\) 块钱的时候,碰到了一个 \((y,2)\),买不起,然后去后面买了一个 \((z,1)\),但其实可以扔掉前面一个 \((x,1)\),满足 \(x+z<y\),替换成 \((y,2)\) 就更优。
我们不妨先把所有 \((a_i,1),(a_i,2)\) 总共 \(2n\) 个数按照题目的要求排好序,我们称这个长度为 \(2n\) 的序列为 \(A\),称某一种具体的定价方案排好序之后的 \(n\) 个数构成的序列为 \(B\)。然后最暴力的想法就是直接枚举 \(x,y,z\),但肯定不是任意这样的三元组都是合法的,具体来说它需要满足:

  1. \(A\) 序列上 \(x,y,z\) 的位置是递增的,且 \(x+z<y\),这是显然的,也是容易判断的。
  2. \(x\) 一定是 \(B\) 序列上 \(y\) 前面最后一个现价为 \(1\) 的糖果,也就是说如果在 \(A\) 序列上 \((x,1),(y,2)\) 之间存在 \((x',1)\),那么 \(x'\) 的现价必须定为 \(2\)
  3. 买到 \(y\) 的时候刚好\(1\) 块钱,也就是说如果 \(B\) 序列上 \(x,y\) 之间夹了一些 \((y',2)\) 那么他们必须都要被买下,同时不算 \(x\) 的话,\(y\) 之前一共花了 \(m-2\) 块。
  4. \(2.\) 类似的,\(z\)\(B\) 序列上 \(y\) 后面第一个现价为 \(1\) 的糖果,即 \(A\) 序列上如果 \((y,2),(z,1)\) 之间存在 \((z',1)\) 那么 \(z'\) 现价必须定为 \(2\)特殊的,这里 \(z\) 允许为空,即 \(B\) 序列上 \(y\) 后面没有任何现价为 \(1\) 的糖果,此时最后会剩下一块钱。

现在我们已经知道了什么样的 \((x,1),(y,2),(z,1)\) 是合法的,但是我们还要计算其他糖果的定价方案数,根据上面的分析,我们发现 \(y\) 这个糖果其实是最重要的(因为四个限制条件都跟 \(y\) 有关),所以很容易从 \(y\) 着手把糖果 \(u\) 分成三类:\((u,2)>(y,2)\)\((u,1)>(y,2)>(u,2)\) 以及 \((u,1)<(y,2)\) ,分别称作一,二,三类点,那么:

  • 三类点:首先 \(z\) 肯定属于三类点,那么根据上面的限制 4.,\(A\) 序列中 \((y,2),(z,1)\) 之间的三类点的定价方案已经确定了,假设 \((z,1)\) 之后的三类点个数为 \(c\),则方案数为 \(2^c\)。当然不要忘了统计 \(z\) 为空的情况,此时三类点的方案数为 \(1\)
  • 一,二类点:此时比较麻烦的是限制 3.,但是根据限制 2. 我们知道 \(A\) 序列上 \((x,1),(y,2)\) 之间的 \((u,1)\) 都不能选,他们必须选 \((u,2)\),如果 \(u\) 是二类点,那么 \((u,2)\) 不会被买,否则会被买,假设这些 \(u\) 中有 \(a_1\) 个一类点,则它们会花费 \(2a_1\)。设在去掉 \(x\) 以及这些 \(u\) 之后有 \(a_2\) 个一类点,\(b\) 个二类点,考虑怎么凑出剩下的 \(m-2-2a_1\) 元,首先 \(a_2\) 个一类点每个都起码贡献一块钱,所以直接从总价中扣掉,现在相当于从 \(a_2+b\) 中选出 \(m-2-2a_1-a_2\) 个糖果,方案数为 \(\binom{a_2+b}{m-2-2a_1-a_2}\)

通过一定的预处理容易 \(O(1)\) 算出方案数,于是直接做就有 \(O(Tn^3),52pts\)
正解只需要枚举 \(x,y\),然后对 \(z\) 用双指针扫就好了,可以轻松做到 \(O(Tn^2)\)

赛时常数写大了,获得 \(92pts\),下面的代码是对赛时代码卡了一些常得到的,可能有点丑,不建议阅读。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,mod=998244353;
inline void Add(int &x,int y){ x=(x+y>=mod)?(x+y-mod):(x+y); }
int fact[N],inv[N],p2[N];
inline int C(int n,int m){
	if(n<0||m<0||n<m) return 0;
	return 1ll*fact[n]*inv[m]%mod*inv[n-m]%mod;
}
int test_id,T,n,m,num,ida[N],idb[N],cnt1,cnt2,cnt3,c[N];
struct P{ int a,w,id; } a[N],b[N],p[N<<1];
inline bool cmp(P x,P y){  //x>y?
	if(x.a*y.w!=y.a*x.w) return x.a*y.w>y.a*x.w;
	else if(x.a!=y.a) return x.a>y.a;
	else return x.id<y.id;
}
inline void calc(int i,int u,int op){
	if(idb[i]<idb[u]) cnt1+=op;
	else if(ida[i]<idb[u]) cnt2+=op;
	else cnt3+=op;
}
void work(){
	cin>>n>>m;
	num=0;
	for(int i=1;i<=n;i++){
		int x; cin>>x;
		a[i]={x,1,i},b[i]={x,2,i};
		p[++num]=a[i],p[++num]=b[i];
	}
	sort(p+1,p+num+1,cmp);
	for(int i=1;i<=num;i++){
		if(p[i].w==2) idb[p[i].id]=i; 
		else ida[p[i].id]=i;
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		int x=idb[i];
		cnt1=cnt2=cnt3=0;
		for(int j=1;j<=n;j++) if(j!=i) calc(j,i,1);
		int tmp=0,tot=0,sum=0;   
		for(int z=x+1;z<=num;z++){
			if(p[z].w==1){
				c[++tot]=p[z].a;
				Add(sum,p2[cnt3-tot]);
			}
		}
		int k=1;
		for(int y=x-1;y>=1;y--){    
			if(p[y].w==2||p[y].id==i) continue;
			int j=p[y].id;
			calc(j,i,-1);
			int res=C(cnt1+cnt2,m-2-tmp-cnt1);
			if(res){
				while(k<=tot&&p[y].a+c[k]>=p[x].a) Add(sum,mod-p2[cnt3-k]),k++;
				Add(ans,1ll*res*sum%mod);
				if(p[y].a<p[x].a) Add(ans,res);
			}
			if(cmp(b[j],b[i])) tmp+=2;
		}
	}
	cout<<(p2[n]-ans+mod)%mod<<'\n';
}
signed main(){
//	freopen("sale.in","r",stdin);
//	freopen("sale.out","w",stdout);
	double beg=clock();
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	p2[0]=fact[0]=inv[1]=inv[0]=1;
	for(int i=1;i<N;i++) p2[i]=p2[i-1]*2%mod;
	for(int i=1;i<N;i++) fact[i]=1ll*fact[i-1]*i%mod;
	for(int i=2;i<N;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=2;i<N;i++) inv[i]=1ll*inv[i-1]*inv[i]%mod;
	cin>>test_id>>T;
	while(T--) work();
	cerr << "Time: " << (clock()-beg) << endl;
	return 0;
}

T3.树的价值

我至今仍未想通为什么我场上一点多项式复杂度都想不出来
\(son(u),subtree(u)\) 分别表示 \(u\) 的儿子集合和子树集合,\(siz(u)=|subtree(u)|\)

考虑树形 DP,我们显然没办法记录子树内现在有哪些数,但是我们知道 \(u\) 子树内 \(>\operatorname{mex}(u)\) 的点对 \(u\) 没有影响,可以之后再考虑他们的权值,于是就有一个状态 \(f_{u,i,j}\) 表示 \(\operatorname{mex}(u)=i\)\(u\) 子树内有 \(j\) 个点的权值还没有确定,子树内的答案。不难做到 \(O(n^3),48pts\)
我们设还没有确定权值的点为空点,初始所有点都是空点,那么我们的过程其实是从下到上考虑每个 \(u\),先指定 \(\operatorname{mex}(u)=\max_{v\in son(u)}(\operatorname{mex}(v))\),接着选择 \(u\) 子树内的 \(k\) 个空点,把他们填上 \([\operatorname{mex}(u),\operatorname{mex}+k)\) 内的数,让 \(\operatorname{mex}(u)\) 加上 \(k\)。所以我们只需要知道每个点使用了多少个空点就可以知道每个点的 \(\operatorname{mex}\) 了(跟这些空点具体的填数方案无关)。但是初始指定 \(\operatorname{mex}(u)=\max_{v\in son(u)}(\operatorname{mex}(v))\) 这个条件很难优化,不过我们发现这里的 \(\max\) 和答案要求的方向相同,对于这种转移和答案方向同号的 DP 题,一种套路是弱化条件,即我们直接钦定 \(\operatorname{mex}(u)\) 一开始等于某个儿子 \(\operatorname{mex}(v)\),发现这样答案不会算多所以是对的。

然后是第一个关键思路,我们发现这个结构(每个点指定一个儿子)其实构成了树的一个链剖分,为了方便,我们称每个点指定的继承 \(\operatorname{mex}\) 的儿子为实儿子,边是实边,其他儿子是虚儿子,边是虚边。对于一个给定的链剖分,假设一个点 \(u\) 到它这条链链顶的路径点数\(d_u\)(或者认为是它在这条链上的深度 \(+1\)),那么他的 \(\operatorname{mex}\) 会贡献到 \(d_u\) 个点,我如果在 \(u\) 这里使用了一个空点,那么总共会让答案加上 \(d_u\)。反过来考虑,对于一个点 \(x\),如果它作为空点被 \(u\) 使用会让答案 \(+d_u\),那么 \(x\) 一定会选择贡献到它到根路径上 \(d\) 最大的点。 所以当链剖分的结构给定时,答案是确定的:\(ans=\sum_{x} \max_{x\in subtree(u)}(d_u)\)。于是我们只需要对这个链剖分的结构 DP,设 \(f_{u,j,k}\) 表示 \(u\) 到根的路径上 \(d\) 最大的点的 \(d=j\)\(d_u=k\)(显然有 \(j\ge k\))时子树内的答案,转移枚举它的实儿子 \(v\),则 \(d_v=d_u+1\) 其他儿子的 \(d=1\)。复杂度 \(O(nm^2),76pts\)(注意要对数据点分治,因为不是所有编号 \(\le 19\) 的点都满足 \(m\le 50\))。

我们首先需要优化状态数量,有两点观察:

  • 观察 1:如果 \(u\) 子树内不存在 \(d_v>j\),则显然 \(u\) 这棵子树的贡献就是 \(j\times siz(u)\),没必要 DP。
  • 观察 2:对于一个链顶 \(u\)\(len(u)\) 表示 \(u\) 这条链的长度(指点数),则如果 \(u\) 为链顶,且 \(len(u)<j\)\(u\) 子树内不会存在另一个链顶 \(v\) 满足 \(len(v)>j\),否则把 \(u\to v\) 上的边全都改为实边(当然会把原先的一些实边变成虚边)一定不劣。

于是可以发现我们只需要保留下面两个状态(\(j\) 的定义和上面 \(O(nm^2)\) 的 DP 是一样的,不多写了):

  • \(f_{u,j}\):表示 \(u\) 是链顶时子树的答案,即上面 \(k=1\)
  • \(g_{u,j}\):表示 \(d_u=j\) 时子树的答案,即上面 \(k=j\)

对于其他 \(j>k>2\) 的状态,我们要么可以直接跳过不 DP,要么可以加速,具体来说:

  1. 对于 \(g\) 的转移:
    • 子树内不存在其他 \(d_v>j\)\(j\times siz(u) \to g_{u,j}\)
    • 指定 \(v\) 为实儿子:\(j+g_{v,j+1}+\sum_{x\in son(u),x\ne v}{f_{x,j}} \to g_{u,j}\)
  2. 对于 \(f\) 的转移:
    • 子树内不存在其他 \(d_v>j\)\(j\times siz(u) \to f_{u,j}\)
    • 否则根据观察 2,\(len(u)\) 一定 \(\ge j\),又因为 \(g\) 的转移可以延长一条链,所以这里我们只需要让 \(len(u)=j\) 即可,那么枚举 \(v\in subtree(u),dep(v)-dep(u)+1=j\) 表示 \(u\) 这条链上深度为 \(j-1\) 的点,则对于 \(u\to v\) 路径上的一个点 \(x\)\(x\ne v\)),它本身的贡献是 \(j\),如果它的一个儿子 \(y\) 不在这条路径上,则 \(y\) 是一条新链的链顶,贡献为 \(f_{y,j}\),所以:\(j(j-1)+g_{v,j}+\sum_{x\in u\to v,x\ne u} h(x,j)\)
      其中 \(h(x,j)\) 表示 \(x\) 的兄弟 \(y\)\(f_{y,j}\) 之和,即 \(h(x,j)=\sum_{y\in son(fa(x)),y\ne x} f_{y,j}\)

这里枚举 \(v\) 的总枚举量是 \(\sum dep_v = O(nm)\) 的。
那么现在唯一的瓶颈在于计算 \(\sum_{x\in u\to v,x\ne u} h(x,j)\),这是一个单点加链求和,可以用树上差分变成子树加单点求和,对每个 \(j\) 开一棵 BIT 维护即可。
时间复杂度 \(O(nm\log n)\),空间复杂度 \(O(nm)\)

点击查看代码
#include<bits/stdc++.h>
#define eb emplace_back
using namespace std;
const int N=8e3+5,M=800+5;
int T,n,m,fa[N],dep[N],f[N][M],g[N][M],siz[N],dfn[N],rev[N],num;
vector<int> G[N];
void dfs0(int u){
	rev[dfn[u]=++num]=u,siz[u]=1;
	for(int v:G[u]) dfs0(v),siz[u]+=siz[v];
}
struct BIT{
	int c[N];
	void init(){ for(int i=1;i<=n;i++) c[i]=0; }
 	void add(int i,int x){ for(;i<=n;i+=(i&-i)) c[i]+=x; }
	int ask(int i,int sum=0){ for(;i;i-=(i&-i))	sum+=c[i]; return sum; }
	void change(int l,int r,int x){ add(l,x),add(r+1,-x); }
}Bit[M];
void dfs(int u){
	for(int v:G[u]) dfs(v);
	for(int j=1;j<=dep[u];j++){
		f[u][j]=g[u][j]=siz[u]*j;
		int sum=0;
		for(int v:G[u]) sum+=f[v][j];
		for(int v:G[u]){
			g[u][j]=max(g[u][j],j+g[v][j+1]+(sum-f[v][j]));
			Bit[j].change(dfn[v],dfn[v]+siz[v]-1,sum-f[v][j]);
		}
	}
	for(int i=dfn[u];i<=dfn[u]+siz[u]-1;i++){
		int v=rev[i],j=dep[v]-dep[u]+1;
		if(j>dep[u]) continue;
		f[u][j]=max(f[u][j],j*(j-1)+g[v][j]+Bit[j].ask(dfn[v]));
	}
}
void Init(){
	num=0;
	for(int i=1;i<=m;i++) Bit[i].init();
	for(int i=1;i<=n;i++){
		G[i].clear();
		for(int j=1;j<=dep[i];j++) f[i][j]=g[i][j]=0;
	}
}
signed main(){
//	freopen("tree.in","r",stdin);
//	freopen("tree.out","w",stdout);
	scanf("%d",&T);
	while(T--){
		Init();
		scanf("%d%d",&n,&m); ++m;
		dep[1]=1;
		for(int i=2;i<=n;i++) scanf("%d",&fa[i]),G[fa[i]].eb(i),dep[i]=dep[fa[i]]+1;
		dfs0(1);
		dfs(1);
		printf("%d\n",g[1][1]);
	}
	return 0;
}

T4.序列询问

题意:\(q\) 次询问,每次询问给出 \(L,R\),对每个 \(i\) 求出包含它且区间长度在 \([L,R]\) 中的最大子段和。单次询问可以 \(O(n)\)

\(ans_i\) 表示 \(i\) 的答案,\(s_i\) 表示原序列的前缀和。
先把子段和拆成前缀和相减的形式,然后考虑一个比较经典的 \(O(nq\log n)\) 做法,对序列分治,只考虑跨过 \(mid\) 的区间 \([l,r]\),然后以左半区间的点 \(i\) 为例,只需要满足 \(l\le i\) 就能让 \(i\)\([l,r]\) 包含,那么我们对每个 \(l\) 用滑动窗口求出合法的最优的 \(r\),把此时的答案记为 \(val_l\),然后对 \(val\) 做一个前缀 \(\max\),再贡献到 \(ans_l\) 即可。
这个做法写的漂亮一点再卡卡常就能获得 \(100pts\) 的部分分

然后正解考虑定长分块 trick,先把原序列以 \(R\) 为块长分块,那么合法区间不可能跨过一整块。对于跨过两个相邻整块分界点的情况,这个分界点就为我们提供了天然的分治中心,套用上面分治的做法即可。
对于整块内的情况,此时已经不用考虑区间长度上界 \(R\) 的问题了,于是我们继续定长分块,把整个块再以 \(L\) 为块长分块,对每个小块 \([x,y]\) 单独考虑,如果区间在小块内显然不合法,否则:

  • 如果最后的区间包含这个小块:一定合法,问题变成求 \(\max_{l\le x,r\ge y}(s_r-s_{l-1})\),因为 \(l,r\) 独立,所以预处理 \(s\) 的前缀 \(\min\) 和后缀 \(\max\) 即可。
  • 如果最后的区间 \([l,r]\) 只有一部分在这个小块内,以 \(l\in [x,y],r\ge \max(y+1,l+L-1)\) 为例:通过预处理的后缀 \(\max\) 可以 \(O(1)\) 得出每个 \(l\) 的答案,然后再把每个 \(l\) 的答案做前缀 \(\max\) 贡献到 \(ans\) 即可。(类似分治部分左半区间的处理方法,只不过由于只有 \(len \ge L\) 一个限制所以不需要滑动窗口)

复杂度 \(O(qn)\),如果你不喜欢滑动窗口可以用 ST 表代替。

洛谷上可以跑进 \(2s\),但是 QOJ 上貌似被 hack 了,不过我懒得卡常了。

点击查看代码
#include<bits/stdc++.h>
#define Debug puts("-------------------------")
#define LL long long
#define ULL unsigned long long 
using namespace std;
const int N=5e4+5;
const LL inf=1e10;
int n,a[N],T,L,R;
LL s[N],ans[N],val[N],pre[N],suf[N];
void chkmax(LL &x,LL y){ if(y>x) x=y; }
struct Deque{
	int dq[N],l,r;
	void init(){ l=1,r=0; }
	bool empty(){ return l>r; }
	int front(){ return dq[l]; }
	int back(){ return dq[r];  }
	void pop_back(){ r--; }
	void pop_front(){ l++; }
	void push_back(int x){ dq[++r]=x; }
}dq; 
void solve(int l,int r){
	pre[l-1]=inf,suf[r+1]=-inf;
	for(int i=l;i<=r;i++) pre[i]=min(pre[i-1],s[i-1]);
	for(int i=r;i>=l;i--) suf[i]=max(suf[i+1],s[i]);
	for(int x=l;x<=r;x+=L){
		int y=min(x+L-1,r);
		val[x-1]=-inf;
		for(int i=x;i<=y;i++){
			if(i+L-1<=r) val[i]=suf[i+L-1]-s[i-1];
			else val[i]=-inf;
			chkmax(val[i],val[i-1]);
			chkmax(ans[i],val[i]);
		}
		val[y+1]=-inf;
		for(int i=y;i>=x;i--){
			if(i-L+1>=l) val[i]=s[i]-pre[i-L+1];
			else val[i]=-inf;
			chkmax(val[i],val[i+1]);
			chkmax(ans[i],val[i]);
		}
		if(x!=l&&y!=r){
			LL V=suf[y]-pre[x];
			for(int i=x;i<=y;i++) chkmax(ans[i],V);
		}
	}
}
void Print(){
	ULL res=0;
	for(int i=1;i<=n;i++) res^=(ULL)i*ans[i];
	printf("%llu\n",res);
}
signed main(){
//	freopen("query.in","r",stdin);
//	freopen("query.out","w",stdout);
	double beg=clock();
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),s[i]=s[i-1]+a[i];
	scanf("%d",&T);
	while(T--){
		scanf("%d%d",&L,&R);
		for(int i=1;i<=n;i++) ans[i]=-inf;
		for(int i=1;i<=n;i+=R) solve(i,min(n,i+R-1));
		for(int l1=1;l1+R<=n;l1+=R){
			int r1=l1+R-1,l2=r1+1,r2=min(n,l2+R-1);
			dq.init();
			val[l1]=-inf;
			for(int i=l1+1,j=l2;i<=r1;i++){
				while(j<=r2&&j-i+1<=R){
					while(!dq.empty()&&s[j]>s[dq.back()]) dq.pop_back();
					dq.push_back(j);
					j++;
				}
				while(!dq.empty()&&dq.front()-i+1<L) dq.pop_front();
				if(dq.empty()) val[i]=-inf;
				else val[i]=s[dq.front()]-s[i-1];
				chkmax(val[i],val[i-1]);
				chkmax(ans[i],val[i]);
			}
			dq.init();
			val[r2+1]=-inf;
			for(int i=r2,j=r1;i>=l2;i--){
				while(j>=l1&&i-j+1<=R){
					while(!dq.empty()&&s[j-1]<s[dq.back()-1]) dq.pop_back();
					dq.push_back(j);
					j--;
				}
				while(!dq.empty()&&i-dq.front()+1<L) dq.pop_front();
				if(dq.empty()) val[i]=-inf;
				else val[i]=s[i]-s[dq.front()-1];
				chkmax(val[i],val[i+1]);
				chkmax(ans[i],val[i]);
			}
		}
		Print();
	}
	cerr << "Time: " << (clock()-beg) << endl;
	return 0;
}
posted @ 2026-01-10 16:01  Green&White  阅读(4)  评论(0)    收藏  举报