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