NOIP2025模拟5

前言:

今天的题目以一种诡异的顺序排列:

\(T2\) 远远简单于 \(T1\)\(T4\) 远远简单于 \(T3\)

?我不理解。

那我赛时死磕 \(T1\) ,赛后死磕 \(T3\) 算什么?

T1:邻面合并(merging)

思路:

赛时两眼一闭就敲轮廓线\(DP\)。敲完了发现 \(gin\) 本不对。

晃荡一个小时,发现合并不了,最后无奈放弃。

其实就是一个纯粹的状压\(DP\)

\(dp_{i,j}\) 表示第 \(i\) 行,状态为 \(j\) 的方案数。

若状态 \(j\) 的第 \(k\) 位为 \(1\) ,则表示 \(s_{i,k}\) 为一个矩形的左起点。

状态设完以后我们还需要写两个函数: \(check\)\(merge\)

这两个函数的作用很显然吧(跟它们的名字一样)。

先说 \(check\) 函数。

一个状态不合法显然只有两种情况:\(s\)\(1\) 但其对应的状态为 \(0\)\(s\)\(0\) 但其对应的状态为 \(1\)

第二个好判,当 !s[k][i]&&(S&t) 时不合法。

问题在于第一个怎么办?

我们只记录了每一个合并前的矩形的起点,但是显然一个矩形是连续的,所以只要原数组中间出现一个 \(0\) ,然后这个状态的某一位置前没有 \(1\) ,该位置的数组值还为 \(1\) 。那一定不合法了。(好像有点绕,建议手模一下有助于理解)

然后我们再来解决 \(merge\) 函数。

怎么合并呢?

用人类大脑烧烤一下,显然是相邻的上下几行中出现状态相同的地方就可以合并(好像有歧义?不过我觉得什么情况下可以合并还是比较好想的吧)

如下图的序列是可合并的。(这里的序列是原始的输入序列)

image

我们设当前行 \(i\) 状态为 \(S\) ,上一行的状态为 \(T\)

我们首先遍历并求一下 \(S\) 中有多少个 \(1\)

然后求出当前 \(1\) 的位置与下一个 \(1\) 的位置,再去看看 \(T\) 中是否相符(具体细节看代码注释)

然后就没有啦~~

代码:

$code$
#include<iostream>
#include<cstring>
using namespace std;
const int N=105,M=10;
int n,m,f[N][1025];
bool s[N][M];
inline bool check(int k,int S){
	for(int i=0;i<m;i++){
		int t=(1<<i);
		if(!s[k][i]&&(S&t)) return 0;
	}//情况二 
	bool flag=0;
	for(int i=0;i<m;i++){
		int t=(1<<i);
		if(S&t) flag=1;//状态中出现 1 
		if(s[k][i]&&!flag) return 0;//不合法的情况 
		if(!s[k][i]) flag=0;//原序列出现 0 ,上一个矩形中断 
	}//情况一 
	return 1;
}
inline int merge(int k,int S,int T){
	int sum=0;
	for(int i=0;i<m;i++){
		int t=(1<<i);
		if(S&t) sum++;
	}//统计 S 中有几个 1 
	for(int i=0;i<m;i++){
		int t=(1<<i);
		if((S&t)&&(T&t)){//都有这个 1 (相同起点) 
			int ed=i;
			while(ed<=m&&s[k][ed]){
				if(ed!=i&&(S&(1<<ed))) break;
				ed++;
			}//下一个 1 
			bool flag=0;
			for(int j=i+1;j<ed;j++){
				if(T&(1<<j)){
					flag=1;
					break;
				}
			}//T在中间中断了 
			for(int j=i+1;j<ed;j++){
				if(!s[k-1][j]){
					flag=1;
					break;
				}
			}//这中间有 0 显然不能合并 
			if(!flag&&((T&(1<<ed))||!s[k-1][ed])) sum--;
		}
	}
	return sum;
}
int main(){
//	freopen("merging.in","r",stdin);
//	freopen("merging.out","w",stdout);
	ios::sync_with_stdio(false);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=0;j<m;j++){
			cin>>s[i][j];
		}
	}//输入 
	memset(f,0x3f,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=n;i++){
		for(int S=0;S<(1<<m);S++){
			if(check(i,S)){//合法状态 
				for(int T=0;T<(1<<m);T++){
					if(f[i-1][T]!=0x3f3f3f3f){
						f[i][S]=min(f[i][S],f[i-1][T]+merge(i,S,T));//合并转移 
					}
				}
			}
		}
	}
	int ans=1e9;
	for(int i=0;i<(1<<m);i++) ans=min(f[n][i],ans);//输出答案 
	cout<<ans<<'\n';
	return 0;
}

T2:家具运输(trans)

思路:

引用: 🐸:仅用 eps 秒想出正解

简简单单的二分答案 + 并不困难的 \(check\) 函数

其实就是二分出题目中给出的 \(w\) ,然后模拟题意就好了。(好像说了跟没说一样)

代码:

$code$
#include<iostream>
#include<algorithm> 
#include<cstring>
using namespace std;
const int N=2005;
int n,k,tot,sum,ans,a[N];
bool vis[N];
inline bool check(int x){
	memset(vis,0,sizeof(vis));
	for(int t=1;t<=k;t++){
		int sum=0;
		for(int i=n;i;i--){
			if(sum+a[i]<=x&&!vis[i]){//从没取过的中取最大的 
				sum+=a[i];
				vis[i]=1;//标记取过 
			}
		}
	}
	for(int i=1;i<=n;i++) if(!vis[i]) return false;//无解 
	return true;
}
int main(){
//	freopen("trans.in","r",stdin);
//	freopen("trans.out","w",stdout);
	ios::sync_with_stdio(false);
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		tot+=a[i];
	}
	sort(a+1,a+1+n);
	int avg=tot/n;
	int l=avg,r=tot;
	while(l<r){//二分答案 
		int mid=(l+r)>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	cout<<r<<'\n';
	return 0;
}

/*
6 2
3 2 4 4 4 7

19 4
2 5 1 12 11 11 15 8 17 19 13 11 3 11 18 13 20 13 3

*/

T3:未完待续。。。

T4:选取字符串(string)

思路:

众所周知,(爸爸的爸爸叫爷爷) \(border\)\(border\) 还是 \(border\)

困困💤:因为一个子串最长前后缀长度为len的话,前后缀长度为len-1的也一定相同,所以当树做,和比他长的连边,siz就是长度为len的个数

所以它们满足一个树形关系。

因此我们可以用 \(KMP\) 求出 \(nxt\) 数组然后构建一个失配树,然后跑树上\(DP\) (当然也可以用纯字符串的🧀)

最后的答案怎么求呢?

首先我们要枚举每一个 \(i\) ,然后从 \(S_i\) 的前缀中选出 \(k\) 个,即组合数 \(C_{siz[i]}^k\)

然后我们需要求出 \(p\)\(q\) 的方案数。

由于每个节点的深度表示它上面有几个节点,对应到本题就是有几个 \(border\)

所以题意可以转化为求 \(\sum _{ S \in \{1,2,...,n\},|S|=k} dep_{lca\{ S \}}^2\)

然后化简一下可得(化简方法同 洛谷P5305

\[\sum (dep_i^2-dep_{fa[i]}^2)=\sum (dep_i+dep_{fa[i]})*(dep_i-dep_{fa[i]}) \]

\[=\sum (dep_i+dep_i-1)*1=\sum (2*dep_i-1) \]

然后直接带式子就好啦~~

代码:

$code$
#include<iostream>
#define int long long
using namespace std;
const int N=1e6+5,mod=998244353;
int k,fac[N],inv[N],nxt[N],dep[N],siz[N],ans;
string s;
inline int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1) res=(res*x)%mod;
		x=(x*x)%mod;
		y>>=1;
	}
	return res;
}
inline int find(int x){
	if(dep[x]) return dep[x];
	if(!x) return dep[x]=1;
	return find(nxt[x])+1;
}
inline int C(int n,int m){
	if(n<m) return 0;
	return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main(){
//	freopen("string.in","r",stdin);
//	freopen("string.out","w",stdout);
	ios::sync_with_stdio(false);
	cin>>k>>s;
	int n=s.size();
	s=' '+s;
	fac[0]=inv[0]=1;
	for(int i=1;i<N;i++) fac[i]=(fac[i-1]*i)%mod;
	inv[N-1]=qpow(fac[N-1],mod-2);
	for(int i=N-2;i;i--) inv[i]=(inv[i+1]*(i+1))%mod;
	siz[0]=2;
	int j=0;
	for(int i=2;i<=n;i++){
		while(j&&s[j+1]!=s[i]) j=nxt[j];
		if(s[j+1]==s[i]) j++;
		nxt[i]=j;
		siz[j]++;
	}//KMP
	for(int i=0;i<=n;i++) if(!dep[i]) dep[i]=find(i);
	for(int i=n;i>=0;i--){
		ans=(ans+(C(siz[i]+(i!=0),k)*(2*dep[i]-1))%mod)%mod;
		siz[nxt[i]]+=siz[i];
	}
	cout<<ans<<'\n';
	return 0;
}
posted @ 2025-11-11 08:05  晏清玖安  阅读(21)  评论(4)    收藏  举报