我的第一篇题解

六眼飞鱼酱① 提交于 2020-03-21 01:10:29

人生第一次写题解

更好阅读体验,请点击这里

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

观察了数据范围之后发现,本题目给出了最大高度的范围。要知道给出数据范围的量是很有可能出现在正解复杂度里的(时间或空间),所以我们尽量往这两个上面靠。由于做题不多,所以我想从仅做过的几个模板题里面借鉴一些思路来解决这个问题。这是本蒟蒻第一篇题解,前面叙述了思路历程,可能前面部分较为啰嗦,想直接看可$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;
} 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!