第一次学习DP,写起来感觉很吃力。
本题的思路参考(其实就是照搬)了这篇文章:CCF-CSP 202104-4 校门外的树(DP/好题) - 脂环 - 博客园 (cnblogs.com)
使用一个数组dp来储存每一步的结果,dp[i]表示在第1个和第 i 个障碍之间存在的方案数。若以第 i 个障碍的坐标与第 j 个障碍的坐标作为等差数列的首/尾元素,则cal(i,j)为可能的等差数列公差的数量。
根据题目所给的归纳定义,可以给出递推公式:
\[dp[i]=\sum_{j=1}^{i-1}(dp[j]\times cal(j,i)) \]关键是计算cal的值。如果自左向右的算,时间复杂度过高,而且无法利用之前步骤得到的数据。所以自右向左的算。先计算 cal(i-1,i) ,计算两个障碍间的距离 d ,然后遍历 d 的所有因子并将其标记为已使用(因子的话在预处理的时候打表)。对小于d 的所有因子,每种因子都对应一种方案。然后计算 cal(i-2,i) ,更新 d 值为障碍 i 和 i-2 间的距离,此时就要用到之前的数据了。假设之前标记了因子 x ,说明如果按照 x 来定义等差数列,那么就要在 i-1 的障碍处种树,这样的话与题设就冲突了,所以在遍历d的因子时,若因子在之前被标记过了,就需要忽略它。
#include<iostream> #include<string.h> #include<vector> #define n_max 1000 #define f_max 100000 #define mod (int)(1e9+7) using namespace std; //障碍物数量 int n; //障碍物坐标 int pos[n_max + 1]; //dp[i]表示a[1]到a[i]的方案数 long long dp[n_max + 1]; //因数表 vector<int> v[f_max + 1]; //标志表 bool flag[f_max + 1]; //打表,求所有数的因数 void Sieve() { for (int i = 1; i <= f_max; i++) { for (int j = i; j <= f_max; j += i) { v[j].push_back(i); } } } void input() { cin >> n; for (int i = 1; i < n + 1; i++) { cin >> pos[i]; } } //以x,y两障碍的坐标为等差数列首尾的值,计算这样的话有几种等差数列 long long cal(int x, int y) { long long count = 0; //两障碍间的距离 int d = pos[y] - pos[x]; for (vector<int>::iterator j = v[d].begin(); j != v[d].end(); j++) { int factor = *j; if (!flag[factor]) { if (factor != d) count++; flag[factor] = true; } } return count; } int main(void) { //打表 Sieve(); //初始化pos memset(pos, 0, (n_max + 1) * sizeof(int)); memset(dp, 0, (n_max + 1) * sizeof(long long)); memset(flag, false, (f_max + 1) * sizeof(bool)); //读输入 input(); //DP求解 dp[1] = 1; for (int i = 2; i <= n; i++) { //将flag恢复 memset(flag, false, (f_max + 1) * sizeof(bool)); for (int j = i - 1; j >= 1; j--) { dp[i] = (dp[i] + (dp[j] * cal(j, i)) % mod) % mod; } } cout << dp[n] << endl; return 0; }
对原文的思路进行了一些小的改进。原文的代码使用了C++STL库的set容器,用于储存、查找被标记的因子。但是set查找的时间复杂度是对数时间,所以原文的代码在 oj 中的运行时间大于900ms;为了改进,我使用了一个大的bool数组储存每个因子的标记情况,这样的话就可以随机访问数组来使查找等的时间降到常数。经过改进,程序在 oj 中的运行时间降到了100ms左右。