20260916NOIP模拟赛#12

前言

其实我觉得所有题全写题解效率太低了,应该未作出的详细写,较简单的写大致思路,有时间再补全,这样的策略更好一点。

题目

高维前缀和

题目描述

给定一个长度为 \(2^n\) 的序列,下标从 \(0\) 到 \(2^n-1\),初始序列的每个数都是 \(1\)。有 \(m\) 次操作,第 \(i\) 次操作形如 \(c_i,d_i,x_i\) 表示将下标满足 \(popcount(j\oplus c_i)=d_i\) 的 \(a_j\) 乘 \(x_i\)。求做完所有 \(m\) 次操作后序列 \(a\) 的每一个数对给定的 \(mod\) 取模的结果。

做法

观察不到对每个操作需要修改的 \(j\) 有什么性质,对每个操作暴力修改所有需要修改的下标是不太能做的。所以对于每个 \(a_j\) 统计所有能作用到的操作的 \(x\) 之积,即 \(j\) 与 \(c_i\) 有 \(d_i\) 位不同的操作的 \(x_i\) 之积,类似高位前缀和,用 dp 逐位考虑是否不同,并记录已经确定的不同的位数。设 \(f_{S,x,v}\) 表示从低到高第 \(x\) 位到第 \(n\) 位中 \(S\) 与 \(c_i\) 相等,且满足 \(v\) 等于 \(d_i-popcount(S\oplus c_i)\) 的操作的 \(x_i\) 之积。初始化每个操作将 \(f_{c_i,0,d_i}\) 乘 \(x_i\),逐位放宽限制,最终每个位置 \(j\) 的答案是 \(f_{j,n,0}\)。

关于转移,枚举当前到第 \(x\) 位、\(S\)、\(v\),如果 \(S\) 该位与操作的 \(c_i\) 相同:

\[f_{S,x,v}\leftarrow f_{S,x,v}\cdot f_{S,x-1,v} \]

否则:

\[f_{S,x,v}\leftarrow f_{S,x,v}\cdot f_{S\oplus 1<<x-1,x-1,v+1} \]

时间复杂度是 \(\mathcal O(n^22^n)\)。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=18;
int n,m,mod,f[N+2][(1<<N)+114][N+2];
int main(){
	freopen("popcount.in","r",stdin);
	freopen("popcount.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>mod>>m;
	for(int i=0;i<=n;i++){
		for(int S=0;S<(1<<n);S++){
			for(int j=0;j<=n;j++)f[i][S][j]=1;
		}
	}
	while(m--){
		int c,d,x;
		cin>>c>>d>>x;
		f[0][c][d]=1ll*x*f[0][c][d]%mod;
	}
	for(int i=1;i<=n;i++){
		for(int S=0;S<(1<<n);S++){
			for(int j=0;j<=n;j++){
				f[i][S][j]=1ll*f[i][S][j]*f[i-1][S][j]%mod*f[i-1][S^(1<<i-1)][j+1]%mod;
			}
		}
	}
	for(int i=0;i<(1<<n);i++)cout<<f[n][i][0]<<" ";
	return 0;
}

dsa

考虑满足 \(\operatorname{popc}(j \oplus x)=d\) 的点。其意义是两个数的不同位置有 \(d\) 个。
在 01-trie 上考虑可以被贡献到的点:

name

如果与 \(x\) 同侧子树内 \(j\) 与 \(x\) 共有 \(d\) 个不同位置,那么异侧的对应点 \(j^{\prime}\) 如果与 \(x\) 有 \((d-1)\) 个不同位置就可以被贡献。

因此与 FWT 的过程类似,按层从下往上考虑,令 \(f_{j, d}\) 表示在当前层,满足与 \(j\) 的不同位置恰好有 \(d\) 个的贡献乘积。对于分治区间 \(\left[i, i+2^{k-1}-1\right]\left[i+2^{k-1}, i+2^{k}-1\right]\) ,计算左边对右边的贡献和右边对左边的贡献进行转移即可:

\[\begin{array}{c} f_{j+2^{k-1}, d-1}^{\prime} \leftarrow f_{j, d} \quad j \in\left[i, i+2^{k-1}-1\right] \\ f_{j-2^{k-1}, d-1}^{\prime} \leftarrow f_{j, d} \quad j \in\left[i+2^{k-1}, i+2^{k}-1\right] \end{array} \]

节点数共有 \(\mathcal O\left(2^{n} n\right)\) 个,每个节点转移枚举 \(d\) 需要 \(\mathcal O(n)\) ,时间复杂度 \(\mathcal O\left(2^{n} n^{2}\right)\) 。

下取整

题目描述

给定一个包含 \(n\) 个互不相同正整数的序列 \(a=(a_1,a_2,\dots,a_n)\),通过将 \(a\) 的元素重新排列为序列 \(b=(b_1,b_2,\dots,b_n)\) 来找到以下表达式的最小值:

\[\left\lfloor \frac{b_2}{b_1} \right\rfloor + \left\lfloor \frac{b_3}{b_2} \right\rfloor + \cdots + \left\lfloor \frac{b_n}{b_{n-1}} \right\rfloor + \left\lfloor \frac{b_1}{b_n} \right\rfloor \]

\(2\leq n\leq 2\times 10^5,1\leq a_i\leq 10^{18}\)

做法

考虑一个序列 \(b\),先找到最大的 \(b_x\),由于 \(b_x\) 最大,它和下一个的贡献为 \(0\),所以可以把 \(x\) 和下一个位置切断,将环变成序列。然后把它画在平面直角坐标系上,对于两个相邻的山峰(先下降后上升的子段),发现合并成一个山峰会更优,因为上升子段中的相邻的两个数的差不会变大,而下降子段没有贡献。将所有的山峰合并完之后会剩下一个山峰和一个上升的后缀,同理也能合并。最终 \(b\) 序列会变成一个先下降后上升的序列,下降的前缀没有贡献,将序列 \(a\) 排序,那么问题就转化成了在 \(a\) 中选包含最小值和最大值的子序列 \(c\) 求 \(c\) 的表达式的最小值。设 \(f_i\) 表示从 \(a_1\) 出发向后跳跳到 \(a_i\) 的最小代价。转移式就是:

\[f_i=\min_{1\leq j<i}\left\lfloor f_j+\frac{a_i}{a_j}\right\rfloor \]

\(f_1=0\),最后答案就是 \(f_n\)。

可以斜率优化。还有一个做法是用二分队列决策单调性优化,还有个更简单的线性转移。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<queue>
using namespace std;
typedef long double ld;
const int N=2e5+5;
int n;
ll f[N],a[N];
bool check(ll x1,ll y1,ll x2,ll y2,ll k){//A(x1,y1),B(x2,y2),判断直线 AB 的斜率是否大于等于 k
	return (__int128)(y2-y1)*x1*x2>=(__int128)k*(x1-x2);
}
bool Check(ll x1,ll y1,ll x2,ll y2,ll X1,ll Y1,ll X2,ll Y2){//A(x1,y1),B(x2,y2),C(X1,Y1),D(X2,Y2),判断直线 AB 的斜率是否大于等于直线 CD 的斜率
	return (ld)(X1-X2)*(y2-y1)/X1/X2>=(ld)(x1-x2)*(Y2-Y1)/x1/x2;
}
deque<pair<ll,ll> >q;//first:a,second:b
int main(){
	freopen("quotient.in","r",stdin);
	freopen("quotient.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	sort(a+1,a+n+1);
	q.clear();
	q.push_back(make_pair(a[1],0ll));
	for(int i=2;i<=n;i++){
		while(q.size()>1&&check(q[q.size()-2].first,q[q.size()-2].second,q[q.size()-1].first,q[q.size()-1].second,-a[i]))q.pop_back();
		f[i]=q[q.size()-1].second+a[i]/q[q.size()-1].first;
		while(q.size()>1&&Check(q[0].first,q[0].second,a[i],f[i],q[1].first,q[1].second,q[0].first,q[0].second))q.pop_front();
		q.push_front(make_pair(a[i],f[i]));
	}
	cout<<f[n];
	return 0;
}

匹配

做法

我去,做过的一点印象都没有。

前后缀 dp,再超级拼装。

救护车

做法

漏了这题没补,大概是背包 dp 再超级拼装。

明天补。

posted @ 2026-09-20 19:13  Tanliu  阅读(24)  评论(0)    收藏  举报