人生第一次写题解
更好阅读体验,请点击这里
简单分析题目之后,发现这个题就是要我们求原数列中有多少等差子数列。
观察了数据范围之后发现,本题目给出了最大高度的范围。要知道给出数据范围的量是很有可能出现在正解复杂度里的(时间或空间),所以我们尽量往这两个上面靠。由于做题不多,所以我想从仅做过的几个模板题里面借鉴一些思路来解决这个问题。这是本蒟蒻第一篇题解,前面叙述了思路历程,可能前面部分较为啰嗦,想直接看可(AC)做法的可以看后面的部分。
状态定义
由于dp的题目我们可以考虑定义前i个元素中选择组成的序列((LCS))或者以i为结尾的序列((LIS))的某个性质的量度(比如(LCS)长度)为(f[i]),当然也可能由于题目有约束(j)(比如背包中的容量),所以就成了定义(f[i][j])为我们想要的状态。在本题中,我先是思考(f[i])为前i个元素中选择能够组成的等差数列的个数,因为看起来对于(i-1<i), (f[i])的组成的一部分就是(f[i-1]),也就是前i个元素中必定不选第(i)个元素时的等差子数列数,那么必然包含第i个元素的时候又是怎样一个结果呢?为了计算这个,我们可能要知道(f[i-1])里面的各种等差子数列的公差细节以及结尾,并根据(a[i])的情况看能否组成等差数列,与我们之前见到的简洁的dp转移不符,暂时放弃此思路。
有了刚才的经验,我们发现可能定义(f[i])为以第i项结尾的等差数列的个数比较好,至少我们知道等差数列的结尾了,在已知(f[j](j<i))的情况下,可以求出(a[i]-a[j]),这时候虽然还不知道(f[j])表示的那些等差数列的公差情况咋样,但我们比刚才又稍微进步了一点。
我们确实没法知道在第二种状态定义下(f[j])中的公差,可能我们还需要别的一些东西。突然想到dp一般是很费空间的,而这个状态定义只需要开1000的数组,有点虚啊!是不是少点东西?回想起最开始说的,题目给出了最大数字不超过20000,这个量是我最开始虽有留心,但前期思考忽略的。这个玩意儿,要么影响时间复杂度,要么影响空间复杂度!考虑到我们刚才弄不出来(f[j])里面的公差,那么,是不是可以人为设定一下公差呢?考虑加一层约束,定义(f[i][k])为以第i个元素结尾的,且公差为k的等差子数列的个数。这下,如果我们已知(f[j][k](j<i)),想求(f[i][k])的话,只需要判断(a[i]-a[j]与k)是否相等就好了。至此,我们心里已经非常有谱了!
转移方程初步思考
上文中说到,在已知所有的(j)的情况下的(f[j][k](j<i))的话,是可以求(f[i][k])的,大概看上去像是在判断公差符合要求的情况下不断求和得到(f[i][k]),用公式表达是:
(f[i][k]=sum_{j}f[j][k]),其中(j<i)且(a[i]-a[j]=k)
这样来看,枚举(i,j,k),会有(O(n^2k))的复杂度,超时是肯定的了。不过,似乎可以再优化一下,毕竟这是我能想到的最可能是正解的思路了。注意到,满足(a[i]-a[j]==k)才能求和,那么一个可能的优化方法是:我们只枚举(k=a[i]-a[j])的情况,也就是说现在的公差完全由(i,j)决定。那么转移方程就成了下面这个情况:
(f[i][a[i]-a[j]]+=f[j][a[i]-a[j]]),其中(j<i)
注意到a[i]-a[j]并非非负,所以要加上一个数,比如说20000,比如说输入数据中最大的高度:
(f[i][a[i]-a[j]+maxheight]+=f[j][a[i]-a[j]+maxheight]),其中(1<=j<i)(假设第一个数字下标为1)
有了这个,这道题的核心就似乎已经被解读出来了。(然而这个方程依然是错的)
转移方程再度思考与细节处理
dp光有转移方程,很多时候也写不好代码,一个原因就是初始化和边界处理,另一个是循环顺序。在这里由于作者水平有限,经常用记忆化搜索规避这个问题,所以不先探讨循环顺序问题,只先说一说边界处理问题,抛砖引玉,希望能给让读者有所启发。
现在“转移方程”在手,我们先试探性地算几个数,看看对不对。比如序列1,2,3
(f[1][0]=1),这是显然的,似乎可以手动初始化一下的亚子
(f[1][1]=?),这个有点懵,不过感觉应该是0吧,先放一放,其他的(f[1][])都算是0吧。
(f[2][0]=1),也手动初始化?似乎有点繁琐哎。
(f[2][1]+=f[1][1]?)不太对啊!(f[1][1]=0),而我们的(f[2][1])算出来是0,但根据样例显然应该是1啊!为什么呢?我最开始以为,(f[i][a[i]-a[j]+maxheight])只是所有的(f[j][a[i]-a[j]+maxheight])的和,但事实不然。考虑到(a[i])与(a[j])在公差为(a[i]-a[j])的情况下,这两项就可以组成等差,它的退化情况(也就是在(f[j][a[i]-a[j]+maxheight]))的情况下应该是只有(a[j]),而显然,只有(a[j])的情况并没有包含在(f[j][a[i]-a[j]+maxheight])中。所以,我们要手动+1去弥补仅有(a[i]与a[j])组成等差数列的情况。所以,修改后的方程为:
(f[i][a[i]-a[j]+maxheight]+=(f[j][a[i]-a[j]+maxheight]+1)),其中(1<=j<i)(假设第一个数字下标为1),+1是为了弥补仅有(a[i]与a[j])组成等差数列的情况。
既然两个元素的边界出了问题,那一个数的会不会也错了呀?如果我们最开始把(f)全部初始化为(0)的话,并且为了避免出岔子,(i)从(2)开始到n进行循环(1比较特殊),确实是丢弃了所有的单元素等差数列的情况,但这让我们的初始化变简单了。作为补偿,在枚举(i,k)对所有(f[i][k])求和之后,要另外加上(n)个单元素等差数列(还有取模),才是答案。就这样,我们通过手动模拟,发现了边界出问题,进而修正了转移方程,并确定了初始化方式,接下来写代码就特别有底气啦!
小优化
在枚举(i,k)对所有(f[i][k])求和,会有(O(nk))的复杂度,是程序的短板。由于我们只需要加那些可行的公差的组成等差数列的情况,所以没必要遍历所有公差,而只需要在dp的时候就边dp边算答案,复杂度(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 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
",ans);
return 0;
}