自信UpUp模拟赛 Day27

自信(自闭)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]=sumdp[i][j1][k]dp[i1][j1][k(i!=j)]dp[t1][j1][k1](k>=(i!=j))(l[j]<=t<=i1)
1、首先我们看,我们可以不放第j个数,就可以从 d p [ i ] [ j − 1 ] [ k ] dp[i][j-1][k] dp[i][j1][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[i1][j1][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[i1][j1][k1]更新的情况。
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[i1][j1][k] d p [ i − 1 ] [ j − 1 ] [ k − 1 ] dp[i-1][j-1][k-1] dp[i1][j1][k1]转移过来,我们发现从后者更新多浪费了步数,与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[i1][j1][k1] d p [ i − 1 ] [ j − 1 ] [ k − 1 ] dp[i-1][j-1][k-1] dp[i1][j1][k1]转移过来,我们发现转移重复,故不成立。
综上,我们不能取 d p [ i − 1 ] [ j − 1 ] [ k − 1 ] dp[i-1][j-1][k-1] dp[i1][j1][k1]
这样的时间复杂度是 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);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值