自信(自闭)UpUp模拟赛 Day27
水池
一、题目及数据范围
题目描述
又到了一年一度的的雨季,幻想乡原来也会下雨。
看着本已经干涸的池塘,灵梦想出了一个高(zhi)深(zhang)的问题:随着雨水落下,池塘中高低不平的地方会积水。给出一个n∗m大小的池塘的每个地方的高度,求雨水落下后每个地方的剩余的雨水的高度。
输入
第一行三个数分别为n, m, L接下来n行m列共n∗m个范围在[0, L]中的整数,分别表示这个地方的高度。
输出
输出包含n行m列,第i行第j列的数表示这个地方的积水的高度。
样例输入
3 3 1
1 1 1
1 0 1
1 1 1
样例输出
0 0 0
0 1 0
0 0 0
数据范围
•对于前40%的数据,n, m≤4, L= 1
•对于100%的数据,n, m≤1000, L≤1000
二、解法
对于40%的数据,写搜索就行,不多说(作者只打的来这种)。
我们来考虑正解,应该很容易看出这是个O(nm)的算法,我先来介绍大佬JZM的并查集算法。
先看这个L,才1000,出题人既然要给我们这个L,一定有它的用处,我们可以考虑桶排。
模拟下雨的过程,对于每一个格子,当它溢出水时,它就一定达到max了,我们知道一个格子的最终状态一定是某一个格子的高度(由于要刚好溢出到边界)。
我们从最矮的格子开始考虑(下雨的过程)。
我们定义 fa[x] 为编号为x的父亲,如果x为根节点,那么我们把它的父亲赋为水柱达到高度(也就是答案),如果一个点所属的并查集的值(指答案)确定了,那么这个点的水就刚好溢出了,如果没有,说明我们需要在跑后面的格子时顺便更新它。形象化的理解,我们可以把存水的地方看成一个凹槽(这就是我们使用桶排+并查集的原因)。
这是大体思路,作者口胡可能不是特别清楚,我们结合代码理解(有注释)
#include <cstdio>
#include <vector>
using namespace std;
const int MAXN = 1005;
int read()
{
int x=0,flag=1;char c;
while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1;
while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*flag;
}
int n,m,l,now,a[MAXN][MAXN],fa[MAXN*MAXN];
int dx[4]={1,-1},dy[4]={0,0,1,-1};
vector<int> t[MAXN];
vector<int> ans;
int findSet(int x)
{
if(fa[x]<=n*m && fa[x]^x)
//不是值或不和自己想等
fa[x]=findSet(fa[x]);
return fa[x];
}
void unionSet(int x,int y)
{
int u=findSet(x),v=findSet(y);//此时的v没有答案
if(u<=n*m)//如果u没有答案,那么把它们连在一起(水槽式连通块)
fa[u]=v;
else//如果u有答案,用它去更新v的答案(相当于并查集又能判联通又能算答案)
fa[v]=u;
}
void solve()
{
for(int i=0;i<=l;i++)
for(int j=0;j<t[i].size();j++)
{
int x=(t[i][j]-1)/m+1,y=(t[i][j]-1)%m+1;//还原位置
fa[m*(x-1)+y]=m*(x-1)+y;//初值
for(int k=0;k<4;k++)
{
int tx=x+dx[k],ty=y+dy[k];
if(tx<1 || tx>n || ty<1 || ty>m ||(fa[m*(tx-1)+ty] && findSet(m*(tx-1)+ty)>n*m))
//如果它不在界内,或在之前就被访问过且已经有了答案。
//这样我们找到格子的高度和它的答案肯定比现在的高度小,再加水一定会溢出
//可能不是很好理解,因为我们的并查集是延迟更新(看下一个循环)
{
fa[m*(x-1)+y]=now;
break;
}
}
for(int k=0;k<4;k++)
{
int tx=x+dx[k],ty=y+dy[k];
if(tx<1 || tx>n || ty<1 || ty>m || fa[m*(tx-1)+ty]==0 || fa[m*(tx-1)+ty]>n*m)
//不在界内或还没有被遍历(等于比现在的高)或有了答案,这样我们对它就没有影响
//我们用这个格子去更新比它矮的,想一想这样的作用
continue;
unionSet(m*(x-1)+y,m*(tx-1)+ty);
}
now++;
ans.push_back(i);//存真正答案,我们之前算的都是下标
}
}
int main()
{
n=read();m=read();l=read();
now=n*m+1;//开始玄学,我们把0给没有遍利过的并查集,1到n*m给节点编号,n*m+1及以后给答案的下标
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
a[i][j]=read();
t[a[i][j]].push_back(m*(i-1)+j);//按高度桶排,hash位置
}
solve();
for(int i=1;i<=n;i++,puts(""))
for(int j=1;j<=m;j++)
printf("%d ",ans[findSet(m*(i-1)+j)-n*m-1]-a[i][j]);//减去初始位置,得到答案
}
排列
一、题目及数据范围
题目描述
琪露诺又开始学数学了,”1+1=?“。 众所周知,琪露诺无法解答这个问题,并对提出这个问题的你感到无比 愤怒,于是提出了一个简单的问题,希望得到你的解答 给出一个长度为n的排列,每次的操作定义为:选择一个区间[l,r],并将 这个区间中的所有数变成这个区间中的最大的数,请问所有的能够通过这样的 操作到达的排列有多少种呢? 但是聪明的你觉得十分简单于是反问琪露诺,如果我限制总的操作次数 那么答案又是多少呢? 琪露诺无法回答这个问题,于是你需要给她答案。
输入格式
第一行包含一个整数T表示数据的组数。 对于每组数据的第一行两个整数n,K表示排列的大小和操作的次数。 接下来一行n个数构成一个排列。
输出格式
输出包含T行,每行输出一个答案, mod 1e9 + 7输出。
二、解法
对于10%的数据,vector+hash直接搜(这才能得10分?)。
对于30%的数据,打暴力dp。
那我们来讲一讲这道题的dp吧,设计
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]为用原序列的前j个数,填生成序列的前i个数,用了k步(不浪费步数),我们可以把这个状转想象成一步一步地填数,以生成序列。
我们可以写出转移。
d
p
[
i
]
[
j
]
[
k
]
=
s
u
m
{
d
p
[
i
]
[
j
−
1
]
[
k
]
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
(
i
!
=
j
)
]
(
k
>
=
(
i
!
=
j
)
)
d
p
[
t
−
1
]
[
j
−
1
]
[
k
−
1
]
(
l
[
j
]
<
=
t
<
=
i
−
1
)
dp[i][j][k]=sum\begin{cases} dp[i][j-1][k]\\ dp[i-1][j-1][k-(i!=j)]&(k>=(i!=j))\\ dp[t-1][j-1][k-1]&(l[j]<=t<=i-1) \end{cases}
dp[i][j][k]=sum⎩⎪⎨⎪⎧dp[i][j−1][k]dp[i−1][j−1][k−(i!=j)]dp[t−1][j−1][k−1](k>=(i!=j))(l[j]<=t<=i−1)
1、首先我们看,我们可以不放第j个数,就可以从
d
p
[
i
]
[
j
−
1
]
[
k
]
dp[i][j-1][k]
dp[i][j−1][k]来转移。
2、我们尝试用第j个数填第i个位置,如果i等于j,那么我们可以直接放,如果不等的话,那我们就要用一次操作把第i为变成j的数,故可以用
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
(
i
!
=
j
)
]
dp[i-1][j-1][k-(i!=j)]
dp[i−1][j−1][k−(i!=j)]。(当然我们要先判
l
[
j
]
<
=
i
<
=
r
[
j
]
l[j]<=i<=r[j]
l[j]<=i<=r[j])
3、我们考虑用第j个数覆盖t到i位,那我们的t为何从i-1开始枚举,这里给出证明:
我们用反证法,考虑从
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]更新的情况。
当
i
=
j
i=j
i=j时,
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]就会从
d
p
[
i
−
1
]
[
j
−
1
]
[
k
]
dp[i-1][j-1][k]
dp[i−1][j−1][k]和
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]转移过来,我们发现从后者更新多浪费了步数,与dp定义冲突,故不成立。
当
i
!
=
j
i!=j
i!=j时,
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]会从
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]和
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]转移过来,我们发现转移重复,故不成立。
综上,我们不能取
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]。
这样的时间复杂度是
O
(
n
4
)
O(n^4)
O(n4)的,还过不了。
我们发现对于k的取值是连续的,故可以维护前缀和,
O
(
1
)
O(1)
O(1)更新dp。
时间复杂度
O
(
n
3
)
O(n^3)
O(n3)。详见代码。
#include <cstdio>
const int MOD = 1e9+7;
int read()
{
int x=0,flag=1;
char c;
while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1;
while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*flag;
}
int T,n,m,ans,a[205],l[205],r[205];
int dp[205][205][205],sum[205][205][205];
int main()
{
T=read();
while(T--)
{
n=read();
m=read();
for(int i=1; i<=n; i++)
a[i]=read();
for(int i=1; i<=n; i++)
{
l[i]=r[i]=i;
while(l[i]>1 && a[l[i]-1]<a[i]) l[i]--;
while(r[i]<n && a[r[i]+1]<a[i]) r[i]++;
}
for(int i=0; i<=n; i++)
dp[0][i][0]=sum[0][i][0]=1;
for(int i=1; i<=n; i++)
for(int j=0; j<=n; j++)
for(int k=0; k<=m; k++)
{
if(j)
{
dp[i][j][k]=dp[i][j-1][k];
if(l[j]<=i && i<=r[j])
{
if(k>=(i!=j)) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k-(i!=j)])%MOD;
if(k-1>=0)
{
if(i>=2) dp[i][j][k]=(dp[i][j][k]+sum[i-2][j-1][k-1])%MOD;
if(l[j]>=2) dp[i][j][k]=(dp[i][j][k]-sum[l[j]-2][j-1][k-1]+MOD)%MOD;
}
}
}
sum[i][j][k]=(sum[i-1][j][k]+dp[i][j][k])%MOD;
}
ans=0;
for(int i=0; i<=m; i++)
ans=(ans+dp[n][n][i])%MOD;
printf("%d\n",ans);
}
}