理解题意后的等价描述:
任意两个障碍物之间可以有美观的插树方案,且该插树方案不能插在障碍物上。n个障碍物任选m个作为端点形成m+1个区间0,在区间进行插树,区间端点和树的位置形成等差数列,且保证树不插在没有选为端点的障碍物上(选为端点的障碍物当然不能插树)。区间内至少插一颗树。
对于计数问题大多是DP问题。我们假设先假设DP问题是一维的,发现一维解决不了问题(不能表示状态或无法完成状态计算)时再把条件作为状态新添维度。
DP
状态:f[i] 前i个障碍物插树的方案数。
属性:计数
划分:固定第i个障碍物作为右端点,其左端点可以是任意j且1 <= j < i,这里左端点j同时作为f[j]的右端点,计数完[i, j]间的插树方案cnt后 f[i] = f[i] + cnt * f[j];
枚举j即可完成f[i]的计算。
问题1:如何枚举插树方案?
假设 $a_j$ 到 $a_i$ 的间距是$g_j$(gap),枚举$g_j$的约数$d | g_j$,且$d\neq{g_j}$(不能插在$a_i$上)。
问题2:如何排除插在障碍物上的方案?(朴素排除超时警告⚠)
开一个数组vis,输入是vis[a[i]] = 1,在枚举$d$的同时枚举a[j] + n * d,如果vis[a[j] + n*d]=1则该$d$不能作为插树方案(插到障碍物上了)
实际上这种枚举a[j] + n * d 的方法会超时。
问题3:如何优化排除插在障碍物上的方案?
我们倒着枚举j,j = [i - 1 .. 1],设$a_j$到$a_i$距离$g_j$,对于任意的k = [j + 1, .. i - 1],设$a_k$到$a_i$距离$g_k$,对于任意的约数$d|g_j$且$d|g_k$,那么$d$必然插到障碍物上。
如果该命题成立,那么我们在枚举$d|g_j$时,如果$d$没有被枚举过,那么res++并标记。否则就必然插在障碍物上。
下面是命题的证明:
如果命题成立,等价于证明从$a_j$开始以d为间距插树必然会插到$a_k$上,即$\exists{n}\in{N^+},a_j + n * d = a_k$,即$d|a_k-a_j$,而$a_k-a_j=g_j-g_k$,由$d|g_k|g_j$可知必然有$d|g_j-g_k|a_k-a_j$,命题得证
注意 j 需要倒着枚举,这样保证每一个$g_k$的约数都枚举过了从而排除对于 j 来说不合法的插树方案。
#include <bits/stdc++.h>
#define int long long
using namespace std;
using pii = pair<int, int>;
const int mod = 1e9 + 7;
int n, a[1005], f[1005], vis[100005];
vector<int> divisor[100005];
signed main() {
#ifdef LOCAL
freopen("in", "r", stdin);
#endif
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
// cout << setiosflags(ios::fixed) << setprecision(2);
// cout << setw(2) << setfill('0');
for (int i = 1; i <= 100000; ++i) {
for (int j = i * 2; j <= 100000; j += i) {
divisor[j].push_back(i);
}
}
cin >> n;
for (int i= 1 ;i <= n; ++i) {
cin >> a[i];
}
f[1] = 1;
for (int i = 2; i <= n; ++i) {
memset(vis, 0, sizeof vis);
for (int j = i - 1; j >= 1; --j) {
int g = a[i] - a[j];
int cnt = 0;
for (auto& x : divisor[g]) {
if (!vis[x]) {
vis[x] = 1;
cnt ++;
}
}
vis[g] = 1;
f[i] = (f[i] + f[j] * cnt % mod) % mod;
}
}
cout << f[n] << '\n';
return 0;
}
%%%
%%%