正睿
DP:
下面均方案的操作顺序若无特殊说明无要求。
例1-1:将一个数 \(n\) 划分成 \(x\) 个正整数,求方案数.(\(n, x \le 5000\))
考虑 dp, \(f_{i,j}\) 表示当前和为 \(i\), 用了 \(j\) 个数的方案数.
那么平凡的转移是 \(f_{i-k,j-1} \rightarrow f_{i,j}\),时间复杂度 \(O(n^3)\),不能通过。
可以把其当成是一个数学问题,有一堆柱子,每次可以加一根柱子,要求最后所有柱子的长度之和为 \(n\)
我们考虑如果加入一个长度为 \(k\) 的柱子,相当于在 \(k-1\) 次操作前加入一个长度为 \(1\) 的柱子,并且每次使其长度 \(+1\),如果是这样,那就只有两种转移了:
- 加入一根柱子
- 集体 +1
那么在不同的时刻加入柱子,最后其长度就是定的,所以可以不重不漏的转移,时间复杂度 \(O(n^2)\).
例1-2:将 \(n\) 划分成不同的正整数,求方案数.(\(n \le 100000\))
由于是不同的,加入的数的个数是 \(O(\sqrt{n})\) 级别的(\(1 + 2 + 3 + ... + \sqrt{n} = O(n)\)),直接 dp 即可,时间复杂度 \(O(n\sqrt{n})\).
例1-3:同 1-2 但是正整数可以相同
根号分治,\(> \sqrt{n}\) 的数只能选 \(\sqrt{n}\) 个,直接 dp 即可,剩下的数像例 1-1 一样跑就行,因为只有 \(\sqrt{n}\) 种不同的数。时间复杂度 \(O(n\sqrt{n})\).
例题:[NOI Online #1 入门组] 跑步
考虑根号分治,\(n<\sqrt{n}\) 时是完全背包,\(n \ge \sqrt{n}\) 时是例 1-1,但这时候加入柱子的长度是 \(\sqrt{n}\) 而不是 \(1\).
Code:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, B = 320;
typedef long long ll;
typedef pair<int, int> pii;
int mod;
void cmax(int &x, int y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; }
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cerr << arg << ' ';
dbg(args...);
}
int n, m, f[N], g[B][N];
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> mod;
m = sqrt(n) + 1;
f[0] = 1;
for (int i = 1; i < m; i++) {
for (int j = i; j <= n; j++) {
add(f[j], f[j - i]);
}
}
g[0][0] = 1;
for (int i = 1; i < m; i++) {
for (int j = i; j <= n; j++) {
g[i][j] = g[i][j - i];
if (j >= m) add(g[i][j], g[i - 1][j - m]);
}
}
int ans = 0;
for (int i = 0; i <= n; i++) {
int sum = 0;
for (int j = 0; j < m; j++) add(sum, g[j][n - i]);
add(ans, 1ll * sum * f[i] % mod);
}
cout << ans << '\n';
return 0;
}
trick: 连续段 dp
维护 \(n\) 个连续段,每次支持三种转移:
- 新建一个段
- 将一个段的两边扩张
- 合并两个段
例题:[CEOI 2016] kangaroo
注意到题目中跳跃的序列是一个 \(1\) 到 \(n\) 的排列,考虑以 \(1\) 到 \(n\) 的 顺序填到某个排列中(先不管 \(s\) 和 \(t\)),设 \(dp_{i,j}\) 表示考虑到 \(i\),目前一共有 \(j\) 段的方案数,上面的转移中,第一种显然是可行的,因为后面填的数肯定大于 \(i\),第二种不行,因为此时 \(i\) 和之前的一个数相邻,比其大,又会比之后填到 \(i\) 另一边的数要小,不符合,第三种显然也可行,\(i\) 比两边的数都大。我们让连续段可以浮动,即其可以任意平移。
那么考虑转移,\(i\neq s \land i \neq t\) 时,就是刚刚的做法,然后由于连续段可以浮动,一共有 \(j - 1 + 1 = j\) 个空可以插,但是注意第一种在 \(i>s \lor i>t\) 时,\(s\) 或 \(t\) 已经填好了,由于 \(s,t\) 的位置固定为 \(1,n\),所以不能放在头或尾段,故转移方程为:
\(dp_{i,j}=dp_{i-1,j-1}\times(j-[i>s]-[i>t])+dp_{i-1,j+1}\times j\)
\(i=s \lor i=t\) 时,必然是 \(s,t\) 在 \(1,n\),所以如果 \(2\) 或 \(n-1\) 被填上了,就是 \(dp_{i-1,j}\),否则是 \(dp_{i-1,j-1}\)。
Code:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 10, mod = 1e9 + 7;
typedef long long ll;
typedef pair<int, int> pii;
void cmax(int &x, int y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; }
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cerr << arg << ' ';
dbg(args...);
}
int n, s, t, dp[N][N];
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> s >> t;
dp[1][1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
if (i != s && i != t) dp[i][j] = (1ll * j * dp[i - 1][j + 1] % mod + 1ll * (j - (i > s) - (i > t)) * dp[i - 1][j - 1] % mod) % mod;
else dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % mod;
}
}
cout << dp[n][1] << '\n';
return 0;
}
字符串
\(i\) 是 \(S\) 的 border \(\Leftrightarrow\) \(|s|-i\) 是 \(s\) 的周期
周期的单调性:如果 \(i\) 是 \(S + c\) (c 为字符)的周期,那么 \(i\) 也是 \(S\) 的周期

浙公网安备 33010602011771号