洛谷 P4933 大师 dp等差提取

转载自BUAA_Wander https://buaa-wander.blog.luogu.org/solution-p4933的洛谷博客。已经本人授权。

思路清晰,尤其关于状态的思考过程,描述极其清晰,详细思忖有大助益。

洛谷P4933大师

简单分析题目之后,发现这个题就是要我们求原数列中有多少等差子数列。

观察了数据范围之后发现,本题目给出了最大高度的范围。要知道给出数据范围的量是很有可能出现在正解复杂度里的(时间或空间),所以我们尽量往这两个上面靠。由于做题不多,所以我想从仅做过的几个模板题里面借鉴一些思路来解决这个问题。这是本蒟蒻第一篇题解,前面叙述了思路历程,可能前面部分较为啰嗦,想直接看可 A C AC 做法的可以看后面的部分。

状态定义

由于dp的题目我们可以考虑定义前i个元素中选择组成的序列( L C S LCS )或者以i为结尾的序列( L I S LIS )的某个性质的量度(比如 L C S LCS 长度)为 f [ i ] f[i] ,当然也可能由于题目有约束 j j (比如背包中的容量),所以就成了定义 f [ i ] [ j ] f[i][j] 为我们想要的状态。在本题中,我先是思考 f [ i ] f[i] 前i个元素中选择能够组成的等差数列的个数,因为看起来对于 i 1 < i i-1<i , f [ i ] f[i] 的组成的一部分就是 f [ i 1 ] f[i-1] ,也就是前i个元素中必定不选第 i i 个元素时的等差子数列数,那么必然包含第i个元素的时候又是怎样一个结果呢?为了计算这个,我们可能要知道 f [ i 1 ] f[i-1] 里面的各种等差子数列的公差细节以及结尾,并根据 a [ i ] a[i] 的情况看能否组成等差数列,与我们之前见到的简洁的dp转移不符,暂时放弃此思路。

有了刚才的经验,我们发现可能定义 f [ i ] f[i] 以第i项结尾的等差数列的个数比较好,至少我们知道等差数列的结尾了,在已知 f [ j ] ( j < i ) f[j](j<i) 的情况下,可以求出 a [ i ] a [ j ] a[i]-a[j] ,这时候虽然还不知道 f [ j ] f[j] 表示的那些等差数列的公差情况咋样,但我们比刚才又稍微进步了一点。

我们确实没法知道在第二种状态定义下 f [ j ] f[j] 中的公差,可能我们还需要别的一些东西。突然想到dp一般是很费空间的,而这个状态定义只需要开1000的数组,有点虚啊!是不是少点东西?回想起最开始说的,题目给出了最大数字不超过20000,这个量是我最开始虽有留心,但前期思考忽略的。这个玩意儿,要么影响时间复杂度,要么影响空间复杂度!考虑到我们刚才弄不出来 f [ j ] f[j] 里面的公差,那么,是不是可以人为设定一下公差呢?考虑加一层约束,定义 f [ i ] [ k ] f[i][k] 以第i个元素结尾的,且公差为k的等差子数列的个数。这下,如果我们已知 f [ j ] [ k ] ( j < i ) f[j][k](j<i) ,想求 f [ i ] [ k ] f[i][k] 的话,只需要判断 a [ i ] a [ j ] k a[i]-a[j]与k 是否相等就好了。至此,我们心里已经非常有谱了!

转移方程初步思考

上文中说到,在已知所有的 j j 的情况下的 f [ j ] [ k ] ( j < i ) f[j][k](j<i) 的话,是可以求 f [ i ] [ k ] f[i][k] 的,大概看上去像是在判断公差符合要求的情况下不断求和得到 f [ i ] [ k ] f[i][k] ,用公式表达是:

f [ i ] [ k ] = j f [ j ] [ k ] f[i][k]=\sum_{j}f[j][k] ,其中 j < i j<i a [ i ] a [ j ] = k a[i]-a[j]=k

这样来看,枚举 i , j , k i,j,k ,会有 O ( n 2 k ) O(n^2k) 的复杂度,超时是肯定的了。不过,似乎可以再优化一下,毕竟这是我能想到的最可能是正解的思路了。注意到,满足 a [ i ] a [ j ] = = k a[i]-a[j]==k 才能求和,那么一个可能的优化方法是:我们只枚举 k = a [ i ] a [ j ] k=a[i]-a[j] 的情况,也就是说现在的公差完全由 i , j i,j 决定。那么转移方程就成了下面这个情况:

f [ i ] [ a [ i ] a [ j ] ] + = f [ j ] [ a [ i ] a [ j ] ] f[i][a[i]-a[j]]+=f[j][a[i]-a[j]] ,其中 j < i j<i

注意到 a [ i ] a [ j ] a[i]-a[j] 并非非负,所以要加上一个数,比如说 20000 20000 ,比如说输入数据中最大的高度:

f [ i ] [ a [ i ] a [ j ] + m a x h e i g h t ] + = f [ j ] [ a [ i ] a [ j ] + m a x h e i g h t ] f[i][a[i]-a[j]+maxheight]+=f[j][a[i]-a[j]+maxheight] ,其中 1 < = j < i 1<=j<i (假设第一个数字下标为1)

有了这个,这道题的核心就似乎已经被解读出来了。(然而这个方程依然是错的)

转移方程再度思考与细节处理

dp光有转移方程,很多时候也写不好代码,一个原因就是初始化和边界处理,另一个是循环顺序。在这里由于作者水平有限,是dp新手,所以经常用记忆化搜索规避这个问题,所以在这儿不先探讨循环顺序问题,只先说一说边界处理问题,抛砖引玉,希望能给让读者有所启发。

现在“转移方程”在手,我们先试探性地算几个数,看看对不对。比如序列1,2,3

f [ 1 ] [ 0 ] = 1 f[1][0]=1 ,这是显然的,似乎可以手动初始化一下的亚子

f [ 1 ] [ 1 ] = ? f[1][1]=? ,这个有点懵,不过感觉应该是0吧,先放一放,其他的 f [ 1 ] [ ] f[1][] 都算是0吧。

f [ 2 ] [ 0 ] = 1 f[2][0]=1 ,也手动初始化?似乎有点繁琐哎。

f [ 2 ] [ 1 ] + = f [ 1 ] [ 1 ] ? f[2][1]+=f[1][1]? 不太对啊! f [ 1 ] [ 1 ] = 0 f[1][1]=0 ,而我们的 f [ 2 ] [ 1 ] f[2][1] 算出来是0,但根据样例显然应该是1啊!为什么呢?我最开始以为, f [ i ] [ a [ i ] a [ j ] + m a x h e i g h t ] f[i][a[i]-a[j]+maxheight] 只是所有的 f [ j ] [ a [ i ] a [ j ] + m a x h e i g h t ] f[j][a[i]-a[j]+maxheight] 的和,但事实不然。考虑到 a [ i ] a[i] a [ j ] a[j] 在公差为 a [ i ] a [ j ] a[i]-a[j] 的情况下,这两项就可以组成等差,它的退化情况(也就是在 f [ j ] [ a [ i ] a [ j ] + m a x h e i g h t ] f[j][a[i]-a[j]+maxheight] )的情况下应该是只有 a [ j ] a[j] ,而显然,只有 a [ j ] a[j] 的情况并没有包含在 f [ j ] [ a [ i ] a [ j ] + m a x h e i g h t ] f[j][a[i]-a[j]+maxheight] 中。所以,我们要手动+1去弥补仅有 a [ i ] a [ j ] a[i]与a[j] 组成等差数列的情况。所以,修改后的方程为:

f [ i ] [ a [ i ] a [ j ] + m a x h e i g h t ] + = j ( f [ j ] [ a [ i ] a [ j ] + m a x h e i g h t ] + 1 ) f[i][a[i]-a[j]+maxheight]+={j}(f[j][a[i]-a[j]+maxheight]+1) ,其中 1 < = j < i 1<=j<i (假设第一个数字下标为1),+1是为了弥补仅有 a [ i ] a [ j ] a[i]与a[j] 组成等差数列的情况。

既然两个元素的边界出了问题,那一个数的会不会也错了呀?如果我们最开始把 f f 全部初始化为 0 0 的话,并且为了避免出岔子, i i 2 2 开始到n进行循环(1比较特殊),确实是丢弃了所有的单元素等差数列的情况,但这让我们的初始化变简单了。作为补偿,在枚举 i , k i,k 对所有 f [ i ] [ k ] f[i][k] 求和之后,要另外加上 n n 个单元素等差数列(还有取模),才是答案。就这样,我们通过手动模拟,发现了边界出问题,进而修正了转移方程,并确定了初始化方式,接下来写代码就特别有底气啦!

小优化

在枚举 i , k i,k 对所有 f [ i ] [ k ] f[i][k] 求和,会有 O ( n k ) O(nk) 的复杂度,是程序的短板。由于我们只需要加那些可行的公差的组成等差数列的情况,所以没必要遍历所有公差,而只需要在dp的时候就边dp边算答案,详细请看代码。在优化之后,时间复杂度为 O ( n 2 ) O(n^2)

代码如下:

#include <bits/stdc++.h>
#define ll long long
#define N 1009
#define V 20008
#define mod 998244353
using namespace std;
ll n,a[N],f[N][2*V],maxh=0,ans=0;//这里int其实够用
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		maxh=max(maxh,a[i]);
	}
	for(int i=2;i<=n;i++){
		for(int j=1;j<i;j++){
			f[i][a[i]-a[j]+maxh]=(f[i][a[i]-a[j]+maxh]+f[j][a[i]-a[j]+maxh]+1)%mod;
			//解释上式为何有+1:这个1指的是a[j]和a[i]这俩元素组成序列的情况,
			//在f[j][a[i]-a[j]]中仅有a[j]并不满足公差条件,所以要单独加上这个 
			ans=(ans+f[j][a[i]-a[j]+maxh]+1)%mod; 
			//我们不是用f[i][a[i]-a[j]+maxh]算的,而是直接加的f[j][a[i]-a[j]+maxh]+1
			//f数组仅用作dp,如果最后再算ans会慢 
		}
	}
	ans=(ans+n)%mod;
	printf("%lld\n",ans);
	return 0;
} 

猜你喜欢

转载自blog.csdn.net/weixin_45502929/article/details/106769794
今日推荐