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\) 函数。
怎么合并呢?
用人类大脑烧烤一下,显然是相邻的上下几行中出现状态相同的地方就可以合并(好像有歧义?不过我觉得什么情况下可以合并还是比较好想的吧)
如下图的序列是可合并的。(这里的序列是原始的输入序列)

我们设当前行 \(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)
然后直接带式子就好啦~~
代码:
$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;
}