CSP202104-4题解析


前言

CSP202104-4题是一道动态规划类题目,对于我这种刚刚接触动态规划的小白来说,有一定难度。本文详细记录做这道题的历程及遇到的一些问题。


一、题目解析

通过读题,我们知道,其实是要将将一个长区间 [ a 0 , a n ) [a_0,a_n) [a0,an)切分成若干个小区间 { [ a i , a j ) ∣ 0 ≤ i < j ≤ n } \{[a_i,a_j)|0 \leq i<j\leq n\} {[ai,aj)0i<jn},每个区间的端点都是两个障碍物。同时我们要在每个区间内找到一个包含两个端点的等差数列,且数列中的数值不能与障碍物坐标相同。最终,我们要求得这样的组合有多少种。由于题目设定大区间的子区间都满足要求时,大区间也满足要求,所以很明显具有递推的性质。

最初,我想当然地选定递推公式如下,其中 i , j , k i,j,k i,j,k均为障碍物标号:
d p ( i , j ) = Σ k = i + 1 j − 1 [ d p ( i , k ) ∗ d p ( k , j ) ] + f ( i , j ) (1) dp(i,j) = \Sigma_{k=i+1}^{ j-1} [dp(i,k)*dp(k,j)]+f(i,j) \tag{1} dp(i,j)=Σk=i+1j1[dp(i,k)dp(k,j)]+f(i,j)(1)
其中 f ( k , j ) f(k,j) f(k,j)代表不在考虑区间 [ a k , a j ) [a_k,a_j) [ak,aj)的子区间满足要求的情况,而仅仅满足这个大区间满足要求的等差数列的数量。那么显然 f f f函数有如下的性质:
d p ( i , i + 1 ) = f ( i , i + 1 ) (2) dp(i,i+1)=f(i,i+1) \tag{2} dp(i,i+1)=f(i,i+1)(2)
但编写完代码测试发现只能过第一个样例,其他地样例结果都偏大。仔细思考发现,这一公式会重复计算很多情况,特别是全部最小的区间都取公差为1的等差数列的情况,上述公式k取任何值时,都会被计入其中。所以这一公式不满足要求。

进一步思考,我将公式修改如下:
d p ( i , j ) = Σ k = i + 1 j − 1 [ d p ( i , k ) ∗ f ( k , j ) ] + f ( i , j ) (3) dp(i,j) = \Sigma_{k=i+1}^{ j-1} [dp(i,k)*f(k,j)]+f(i,j) \tag{3} dp(i,j)=Σk=i+1j1[dp(i,k)f(k,j)]+f(i,j)(3)

基于上面(2)(3)两个公式,我们就可以完成所有的递推,在实际代码编写,我们只需完成 f f f函数即可。

二、代码编写及时间优化

1.注意事项及时间优化

在编写完基本代码后,又发现样例3结果错误,测试也只能过3个点,盯着代码望了许久,想起来自己没考虑数据溢出的问题,dp数组还用的 i n t int int类型,改成 l o n g   l o n g long \ long long long后就A了6个点,接下来的主要问题就是超时的问题,显然这里唯一能优化的地方就是我们的 f f f函数。

最初,我使用的 f f f函数是暴力扫描,即扫描 [ a i , a j ) [a_i,a_j) [ai,aj)遍历所有可能的公差,判断是否会与障碍物重合,找出其中可行的公差,这显然在数据规模大了以后是很费时的。

这时,我们要考虑数据的复用性。如果 d d d是满足 [ a i , a j ) [a_i,a_j) [ai,aj)要求的公差,那么d一定不是 [ a i − 1 , a j ) [a_{i-1},a_j) [ai1,aj)要求的公差,因为在后者中 a i a_i ai成为区间内障碍物,使用 d d d必然会出现重合的情况。据此,我们可以优先计算子区间的公差并保存,然后在大区间中搜索时,可以直接排除掉子区间的公差。

除此之外,可以发现公差一定是区间长度的因数。提前给所有不大于 a n a_n an的正整数打一个因素表,可以避免掉逐个搜索的费时操作。在实操中,我们可以直接计算区间长度,然后读表,对比该因数是否是子区间的公差即可。

2.最终代码展示

下面最终的代码展示,不再有超时的问题。
#include <bits/stdc++.h>
using namespace std;
#define M 1000000007
const int MaxN= 1000;
int a[MaxN+1];
long long dp[MaxN+1];           //dp数组
vector<int> Divor[100001];      //因素表
set<int> value;                    //子区间的公差集合
int f(int x,int y)
{
    int X=a[y]-a[x];
    value.insert(X);
    int cnt=0;
    for(int i=0;i<Divor[X].size();i++)
    {
        //对比因数和子区间公差
        if(value.find(Divor[X][i])!=value.end()) continue;
        else
        {
            value.insert(Divor[X][i]);
            cnt++;
        }
    }
    return cnt;
}

int main()
{
    
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);

    //打表
    for(int i=1;i<=a[n]/2;i++)
        for(int j=2*i;j<=a[n];j+=i)
            Divor[j].push_back(i);
    
    //递推
    for(int i=2;i<=n;i++)
    {
        //每一次清空子区间公差集合
        value.clear();
        //要从小区间开始递推
        for(int j=i-1;j>1;j--)
            dp[i] = (dp[i] + dp[j] * f(j,i)% M) % M;
        dp[i] = (dp[i] + f(1,i)) % M;
    }
    cout<<dp[n];
    system("pause");
    return 0;
}

总结

部分优化思想来源于以下链接内容,特此感谢!

😕/www.manongjc.com/detail/25-oxfdqgqiujegtic.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值