我的第一篇题解
人生第一次写题解
更好阅读体验,请点击这里
简单分析题目之后,发现这个题就是要我们求原数列中有多少等差子数列。
观察了数据范围之后发现,本题目给出了最大高度的范围。要知道给出数据范围的量是很有可能出现在正解复杂度里的(时间或空间),所以我们尽量往这两个上面靠。由于做题不多,所以我想从仅做过的几个模板题里面借鉴一些思路来解决这个问题。这是本蒟蒻第一篇题解,前面叙述了思路历程,可能前面部分较为啰嗦,想直接看可\(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\n",ans);
return 0;
}