[47] (CSP 集训) CSP-S 模拟 11
A.玩水
注意到只有在形如下面这样的地方才会发生分叉
?a
a?
所以 \(f_{i,j}\) 表示从 \((1,1)\) 到 \((i,j)\) 的矩阵中的最大路径方案数,考虑转移
普通的转移是 \(f_{i,j}=\max(f_{i,j-1},f_{i-1,j})\)
如果 \(a_{i-1,j}=a_{i,j-1}\),则有 \(f_{i,j}=2f_{i-1,j-1}\)
因为 \((i-1,j-1)\) 处的方案数都可以通过向下或者向右到达 \((i,j)\),所以乘二
这个东西是假的,但是只会在下面这两种时候假
ab
bc
cd
abc
bcd
也就是两个 \(a_{i-1,j}=a_{i,j-1}\) 是相邻的,这个时候 DP 就会出错
但是你可以发现,一旦有相邻的 \(a_{i-1,j}=a_{i,j-1}\),那么方案数就一定大于等于 \(3\) 了,此时直接返回即可
因此先把这种情况判掉,再做 DP 就行了
#include<bits/stdc++.h>
using namespace std;
char mp[1001][1001];
int f[1001][1001];
int n,m;
bool check(){
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
if(mp[i][j-1]==mp[i-1][j]){
if((i>2 and j>1 and mp[i-1][j-1]==mp[i-2][j]) or (i>1 and j>2 and mp[i][j-2]==mp[i-1][j-1])){
return true;
}
}
}
}
memset(f,0,sizeof f);
f[1][1]=1;
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
if(i==1 and j==1) continue;
if(i!=1) f[i][j]=max(f[i][j],f[i-1][j]);
if(j!=1) f[i][j]=max(f[i][j],f[i][j-1]);
if(i!=1 and j!=1 and mp[i][j-1]==mp[i-1][j]){
f[i][j]=max(f[i][j],f[i-1][j-1]*2);
}
}
}
return f[n][m]>=3;
}
int t;
int main(){
scanf("%d",&t);
while(t--){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;++i) scanf("%s",mp[i]+1);
if(check()) printf("1\n");
else printf("0\n");
}
}
B.AVL树
先序遍历字典序优先 = 中序遍历字典序优先
所以按照先序便利顺序考虑每个点,如果能够加入就加入,不能加入则跳过该子树
首先有一个非常好的递推性质:一颗深度为 \(i\) 的 AVL 最少的节点数 \(fi=fi−1+fi−2+1\)
然后就是贪心,顺次考虑每个点时,我们每次向上跳,如果当前点作为左子树,算出右子树至少需要加入多少点,如果加入当前点需要加入的点数小于等于剩余点数就加入
需要维护当前节点的最大子树深度,子树深度的合法下界,这都是可以通过一遍 dfs 确定的
#include<bits/stdc++.h>
using namespace std;
int n,k;
int res,root;
int tol[500001],tor[500001];
int fa[500001];
int maxn[500001],minn[500001];
int deep[500001],ndeep[500001];
bool flag[500001];
int f[500001];
void prework(int now){
maxn[now]=deep[now]=deep[fa[now]]+1;
if(tol[now]){
prework(tol[now]);
maxn[now]=max(maxn[now],maxn[tol[now]]);
}
if(tor[now]){
prework(tor[now]);
maxn[now]=max(maxn[now],maxn[tor[now]]);
}
}
int check(int now){
int ndep=max(deep[now],ndeep[now]);
int ans=1;
while(fa[now]){
bool left=(tol[fa[now]]==now);
now=fa[now];
ndep=max(ndep,ndeep[now]);
if(left and tor[now]){
ans+=f[max(ndep-1,minn[tor[now]])-deep[now]];
}
}
return ans;
}
void update(int now){
int ndep=ndeep[now]=max(ndeep[now],deep[now]);
while(now){
now=fa[now];
ndeep[now]=ndep=max(ndeep[now],ndep);
if(tor[now]){
minn[tor[now]]=max(minn[tor[now]],ndep-1);
}
}
}
void dfs(int now){
int cnt=check(now);
if(cnt<=res){
update(now);
flag[now]=true;
res--;
if(tol[now] and tor[now]){
int dt=maxn[tol[now]]<minn[now];
minn[tol[now]]=max(minn[tol[now]],minn[now]-dt);
minn[tor[now]]=max(minn[tor[now]],minn[now]+dt-1);
dfs(tol[now]);
dfs(tor[now]);
return;
}
if(tol[now]){
minn[tol[now]]=max(minn[tol[now]],minn[now]);
dfs(tol[now]);
}
if(tor[now]){
minn[tor[now]]=max(minn[tor[now]],minn[now]);
dfs(tor[now]);
}
}
}
int main(){
freopen("avl.in","r",stdin);
freopen("avl.out","w",stdout);
cin>>n>>k;res=k;
for(int i=1;i<=n;++i){
int x;cin>>x;
if(x==-1) root=i;
else{
fa[i]=x;
if(i>x) tor[x]=i;
else tol[x]=i;
}
}
f[1]=1;f[2]=2;
for(int i=3;i<=n;++i){
f[i]=(f[i-1]+f[i-2]+1);
}
// cout<<"?";
prework(root);
// cout<<"??";
dfs(root);
// cout<<"???";
for(int i=1;i<=n;++i){
cout<<flag[i];
}
}
C.暴雨
设 \(f_{i,j,k,0/1}\) 表示考虑前 \(i\) 个,其中最大值高度为 \(j\)(这一维没法开,记录排名),后面也存在一个高度不小于 \(j\) 的土块(即 \(j\) 高度能造成贡献(最终水位高度为 \(j\),这样就能直接转移的时候计算每个格子的终态贡献了)),并且前 \(i\) 个中推平了 \(k\) 个,当前积水的体积是奇数/偶数的总方案数
你会发现这么个东西没办法开,但是我们第二维可以只开 \(k\) 个,因为只有最高的 \(K(+1)\) 个值会对答案有贡献,即使你把所有最高的 \(K\) 个全推平了,也不会轮到 \(K(+1)\) 以外的当最高值
分别前后算,枚举中转点统计答案,要枚举中转点是因为我们在内层设计 DP 数组的时候钦定一定在后面有一个不小于排名 j 的值的元素,所以这里枚举中转点的时候对接一下
有几个辅助数组不开做不了,主要是排名和值的转换有关的
懒得喷,注释啥的都在代码里,写的挺细的
UPD
牛魔,怎么还有看不懂注释的
假设你一开始什么数都不填,先钦定一个终态最小值 \(j\)(这就要求你填的数不能超过 \(j\)),那么,如果你现在填入一个高度为 \(h\) 的数字,有以下两种情况
- \(h\le j\),由于终态已经确定,此时你可以直接算出在这一位产生的贡献,即 \(j-h\)
- \(h\ge j\),此时已经不满足终态是 \(j\) 的限制,你可以通过拉高 \(j\) 来解决这个问题(也就是说,这个情况要求你从 \(f_{i,j,k,l}\) 转移到一个 \(f_{i+1,h,k',l'}\))
但是这些贡献能被算出是有条件的,那就是必须有一个大于等于 \(j\) 的值来兜底,否则你存的这些水就流出边界了,这个条件后面会用到
然后就是怎么判断状态能被统计到答案里
首先我们对序列正着倒着分别跑一遍,统计出答案,可以想到,我们应该枚举一个中转点(这个中转点是极大的,它可以将整个序列分成互不相关的左右两部分),然后将左右两边的方案数相乘
现在你枚举这个最高的兜底的值,然后判断它是否能作为两边的 “兜底” 的值(也就是它是否大于等于两边的 \(j\)),如果可以,那么就统计进答案里
理解了这个思路,推平操作就很简单了,当你从 \(i\) 转移到 \(i+1\) 的时候,考虑是否推平 \(i+1\),如果 \(a_{i+1}\) 更大,推平可以导致 \(j\) 不发生变化,否则只会导致对高度的贡献由 \(j-h\) 变成 \(j-0\)
#include<bits/stdc++.h>
using namespace std;
const int p=1e9+7;
int n,k,ans;
int h[25001],id[25001];
struct DP{
//记录需要 DP 的数组
//因为会有倒着做的所以就加了一个这个
int a[25001];
//考虑前 i 个,其中最大值高度为 j(这一维没法开,记录排名),后面也存在一个高度不小于 j 的土块
//(即 j 高度能造成贡献)
//并且前 i 个中推平了 k 个,当前积水的体积是奇数/偶数
//的总方案数
int f[25001][26][26][2];
//排名只记录 K 个是因为,只有最高的 K(+1) 个值会对答案有贡献,即使你把所有最高的 K 个全推平了
//也不会轮到 K(+1) 以外的当最高值
//记录前 i 个数中第 j 大的值
int val[25001][26];
int cnt[25001];
//set 用于维护 val 数组
set<int> s;
//查某个 val 在 i 中对应的排名
map<int,int> mp[25001];
inline void operator()(){
s.insert(0);
for(int i=1;i<=n;++i){
s.insert(a[i]);
auto iter=s.end();
for(int j=1;j<=k+1;j++){
if(iter==s.begin()) break;
iter--;
mp[i][*iter]=++cnt[i];
val[i][cnt[i]]=*iter;
}
}
f[0][1][0][0]=1;
cnt[0]=1;
val[0][1]=0;
for(int i=0;i<n;i++){ //从 i 向 i+1 转移
for(int j=1;j<=cnt[i];j++){
for(int l=0;l<=k;l++){
for(int u=0;u<=1;u++){
if(f[i][j][l][u]){
if(val[i][j]>=a[i+1]){ //新增的柱子更低,最高值不变
//这里形如 mp[i+1][val[i][j]] 的写法,实际上是求 “在前 i 个数中排名为 j 的数在前 i+1 个数中的排名”,也就是最值没变(只是排名可能变了)
f[i+1][mp[i+1][val[i][j]]][l][(u+val[i][j]-a[i+1])%2]=(f[i+1][mp[i+1][val[i][j]]][l][(u+val[i][j]-a[i+1])%2]+f[i][j][l][u])%p; //新增的数做出两者高度差的贡献(因为状态设计的时候说一定有一个)
//后面的柱子大于等于排名 j 的值,所以这里直接加上 [a[i+1],val[i][j]] 之间的所有贡献
if(l+1<=k) f[i+1][mp[i+1][val[i][j]]][l+1][(u+val[i][j])%2]=(f[i+1][mp[i+1][val[i][j]]][l+1][(u+val[i][j])%2]+f[i][j][l][u])%p; //推平 i+1,加上 [0,val[i][j]] 之间的贡献
}
else{ //新增的柱子更高
f[i+1][mp[i+1][a[i+1]]][l][u]=(f[i+1][mp[i+1][a[i+1]]][l][u]+f[i][j][l][u])%p; //最高的数是 a[i+1],新增的数不作贡献(a[i+1]-a[i+1]=0)
//这里更改了最高的值,不再从原数组转移贡献
//相当于你从中间截断了,之前的贡献(被截开的前半段)计算了就不会变了,下面你对这个最高值的转移计算的是后半段的贡献
if(l+1<=k) f[i+1][mp[i+1][val[i][j]]][l+1][(u+val[i][j])%2]=(f[i+1][mp[i+1][val[i][j]]][l+1][(u+val[i][j])%2]+f[i][j][l][u])%p; //推平 i+1,最高值不变,直接造成整个贡献
}
}
}
}
}
}
}
};
//分别前后算,枚举中转点统计答案
DP dpforward,dpbackward;
int main(){
ios::sync_with_stdio(false);
freopen("rain.in","r",stdin);
freopen("rain.out","w",stdout);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>h[i];
dpforward.a[i]=dpbackward.a[n-i+1]=h[i];
id[i]=i;
}
dpforward();dpbackward();
sort(id+1,id+n+1,[](int x,int y){return h[x]>h[y];});
//因为我们在内层设计 DP 数组的时候钦定一定在后面有一个不小于排名 j 的值的元素,所以这里枚举中转点的时候对接一下
for(int i=1;i<=n;i++){
if(h[id[i]]<h[id[k+1]]) break; //只枚举前 K+1 个元素,当然你这里写成 i<=K+1 会错,因为可能有不止一个元素值等于 K+1 的
for(int j=dpforward.cnt[id[i]-1];j;--j){
if(dpforward.val[id[i]-1][j]>h[id[i]]) break;
for(int l=dpbackward.cnt[n-id[i]];l;--l){
if(dpbackward.val[n-id[i]][l]>=h[id[i]]) break;
for(int u=0;u<=k;u++){
ans=(ans+1ll*dpforward.f[id[i]-1][j][u][0]*dpbackward.f[n-id[i]][l][k-u][0]%p)%p;
ans=(ans+1ll*dpforward.f[id[i]-1][j][u][1]*dpbackward.f[n-id[i]][l][k-u][1]%p)%p;
}
}
}
}
cout<<ans%p;
}
后面的仙姑
私は雨 / 稻叶昙
私は誰 あなたの哀れ
夜空の中で 名前を無くして
うねりのない 水面に潜む景色を
知らないまま (霧になってしまっても)
漂う雲 (別にいいのに)
昨日までは (構わないのに) 漂う雲
私はなぜ 真っすぐに落ちる
だれかの手のひらを探すため
空をできる限り 目に収めながら
私は雨 弾かれて判る
だれかのようにはなれない雨
地球を困らせるほどの痛みを知らないから
私は雨 セカイを暈す
夜明けに導かれている雨
流れ着いた海の隠し味を知るまで
星を隠した雷鳴と
視界からはみ出した 積乱雲
できるだけ できるだけ できるだけ
離れていたかった
傘をさす 余裕はないし
このままでもいいと思えるよ
わからないから 染み込んでるの
夜の強い雨で 目を覚ます
私は雨 地球をなぞる
一粒では気付くことのない雨
夜空に飾り付ける 星を見つけて
空に浮かんだり 地に足をつけたり
消えかかったり 溢れかえったりする
描けていたら 何も起きなかった
セカイ的気候変動
私は雨 滴って判る
だれかのようにはなれない雨
地球を困らせるほどの思いを知りたいから
私は雨 セカイを暈す
夜明けに導かれている雨
流れ着いた海の隠し味になるまで
私は雨
辿り着くまでに
おさらいを忘れないで
凪の海で向かい合わせ
違う景色 同じ模様の 答え合わせ
/> フ
| _ _|
/`ミ x 彡
/ |
/ ヽ ?
/ ̄| | | |
| ( ̄ヽ_ヽ))
\二つ