终于意识到动规的水平差得一批了。这边开个小专题练一下。
题目列表是学长 frank_c1 的博客。这样一来可以保证质量,二来也有自己能看懂的题解。
事实上很多题意也是贺的
这里还是挂个链接吧:orz
(刷新以获取数学公式)
[APIO 2014] Beads and wires
题意
在达芬奇时代,有一个流行的儿童游戏称为连珠线。当然,这个游戏是关于珠子和线的。线是红色或蓝色的,珠子被编号为 $ 1 $ 到 $ n $ 。这个游戏从一个珠子开始,每次会用如下方式添加一个新的珠子:
- $ Append(w, v) $ :一个新的珠子 $ w $ 和一个已经添加的珠子 $ w $ 用红线连接起来。
- $ Insert(w, u, v) $ :一个新的珠子 $ w $ 插入到用红线连起来的两个珠子 $ u $ , $ v $ 之间。具体过程是删去 $ u $ , $ v $ 之间红线,分别用蓝线连接 $ u $ , $ w $ 和 $ w $ , $ v $ 。
每条线都有一个长度。游戏结束后,你的最终得分为蓝线长度之和。
给你连珠线游戏结束后的游戏局面,只告诉了你珠子和链的连接方式以及每条线的长度,没有告诉你每条线分别是什么颜色。
你需要写一个程序来找出最大可能得分。即,在所有以给出的最终局面结束的连珠线游戏中找出那个得分最大的,然后输出最大可能得分。
数据范围: $ 1≤n≤200000 $
题解
如果确定一个点为根,我们可以且仅可以进行一个操作:选择两条有父子关系的边,将他们同时染蓝。
$ O(n^2) $ 的 $ DP $ 就是枚举根, $ f_{x,0/1} $ 表示 $ x $ 这棵子树中, $ x $ 到它父亲这条边 用了/没用 能产生的最大贡献。
方程列出来发现可以换根 $ DP $ ,操作一下就做完了。
(事实上我一开始用点分治做了换根 $ DP $ 做的事情,虽然本质上一样,但是一个上午都没调出来)
#include<bits/stdc++.h>
#define LL long long
#define pii pair<LL, LL>
using namespace std;
const int N = 2e5 + 10;
const int inf = 0x3f3f3f3f;
int n, m, E;
int fir[N], nex[N << 1], arr[N << 1], len[N << 1], flen[N];
LL ans, f[N][2];
set<pii> Set[N];
void Add_Edge(int x, int y, int l) {
nex[++E] = fir[x];
fir[x] = E; arr[E] = y; len[E] = l;
}
void dfs(int x, int fa) {
f[x][0] = f[x][1] = 0;
for (int i = fir[x]; i; i = nex[i]) {
if (arr[i] == fa) continue;
int y = arr[i];
flen[y] = len[i];
dfs(y, x);
f[x][1] += max(f[y][0], f[y][1]);
Set[x].insert(make_pair(max(f[y][0], f[y][1]) - flen[y] - f[y][1], arr[i]));
}
if (Set[x].size()) {
f[x][0] = f[x][1] - (Set[x].begin() -> first) + flen[x];
}
}
void dfs2(int x, int fa) {
ans = max(ans, f[x][1]);
for (int i = fir[x]; i; i = nex[i]) {
if (arr[i] == fa) continue;
int y = arr[i];
LL f0 = f[y][0], f1 = f[y][1];
f[x][1] -= max(f0, f1);
Set[x].erase(make_pair(max(f0, f1) - len[i] - f[y][1], y));
f[x][0] = 0;
if (Set[x].size()) {
f[x][0] = f[x][1] - (Set[x].begin() -> first) + len[i];
}
f[y][1] += max(f[x][0], f[x][1]);
f[y][0] = f[y][1];
Set[y].insert(make_pair(max(f[x][0], f[x][1]) - len[i] - f[x][1], x));
dfs2(y, x);
Set[y].erase(make_pair(max(f[x][0], f[x][1]) - len[i] - f[x][1], x));
f[y][1] = f1;
f[y][0] = f0;
f[x][1] += max(f0, f1);
Set[x].insert(make_pair(max(f0, f1) - len[i] - f[y][1], y));
f[x][0] = f[x][1];
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int x, y, l;
scanf("%d%d%d", &x, &y, &l);
Add_Edge(x, y, l);
Add_Edge(y, x, l);
}
dfs(1, 0);
dfs2(1, 0);
cout << ans << endl;
return 0;
}
[ZJOI 2017] 仙人掌
题意
给定 $ n $ 点 $ m $ 边的简单无向连通图。
现在要在图上加若干边,要求加边后还是一个简单无向连通图,且每条边至多属于一个简单环。
求加边方案数对 $ 998244353 $ 取模的值。
$ 1≤n≤500000 $ , $ 1≤m≤1000000 $
题解
如果原图是不是仙人掌,答案为 $ 0 $ 。
一张图为仙人掌当且仅当 $ dfs $ 树上的边最多被非树边覆盖 $ 1 $ 次。
已经被覆盖过的树边不能再次被覆盖。直接断开这些边,问题就转化为树上的情况。
因为不能有重边,所以问题就相当于:选出若干长度不小于 $ 2 $ 的路径,每条边不能被覆盖超过 $ 1 $ 次,求方案数。
这里一个关键的转化是,上述问题等价于:用若干条长度不小于 $ 1 $ 的路径,覆盖每条边恰好一次。
用 $ f_x $ 表示子树 $ x $ 的答案, $ g_x $ 表示子树 $ x $ 中,存在且仅存在一条从 $ fa_x $ 引下的链,其余边都被覆盖的方案数。
这里我转移时又糊了假算法。我加入一棵子树后直接转移,如果 $ x $ 保留了来自 $ y $ 的链,现在加入子树 $ z $ ,要么 $ y $ 和 $ z $ 连转移到 $ f_x $ ,要么 $ z $ 和 $ x $ 连转移到 $ g_x $ ,但是忽略了可以同时保留 $ y,z $ 两条链,分别和后面 $ u,v $ 两条相连的情况,于是过不了大样例
转移时先处理出一个数组 $ h $ , $ h_n $ 表示 $ n $ 棵子树,每棵子树有 $ 1 $ 条从 $ x $ 引下的链,处理到所有边都被覆盖的方案数。
转移就很显然了,设 $ x $ 的儿子个数为 $ cnt $ :
$ h_0 = h_1 = 1, h_n = h_{n-1} + (n-1) h_{n-2} (n≥2) $
$ f_x = h_{cnt} \Pi_{fa_y = x} g_y $
$ g_x = f_x + \sum_{fa_y = x} h_{cnt - 1} \Pi_{fa_z = x} g_z $
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
const int M = 2e6 + 10;
const int mod = 998244353;
const int inf = 0x3f3f3f3f;
int n, m, E;
int h[N], f[N], g[N], vis[N], tag[N];
int fir[N], nex[M], arr[M], del[M], dep[N];
inline void Add_Edge(int x, int y) {
nex[++E] = fir[x];
fir[x] = E; arr[E] = y; del[E] = 0;
}
int dfs(int x, int pre) {
int res = 0;
vis[x] = 1;
for (int i = fir[x]; i; i = nex[i]) {
if (!vis[arr[i]]) {
dep[arr[i]] = dep[x] + 1;
res += dfs(arr[i], i);
}
else if ((i ^ 1) != pre && vis[arr[i]] && dep[x] > dep[arr[i]]) {
++res, --tag[arr[i]];
del[i] = 1;
del[i ^ 1] = 1;
}
}
res += tag[x];
if (res) {
del[pre] = 1;
del[pre ^ 1] = 1;
}
if (res > 1) return -inf;
return res;
}
int add(register int x, register int y) {
static int z;
return ((z = (x + y)) >= mod) ? z - mod : z;
}
void dfs2(int x) {
vis[x] = 1;
int cnt = 0, s = 1;
for (int i = fir[x]; i; i = nex[i]) {
if (vis[arr[i]] || del[i]) continue;
dfs2(arr[i]);
++cnt;
s = 1LL * s * g[arr[i]] % mod;
}
if (!cnt) {
f[x] = 1;
g[x] = 1;
}
else if (cnt) {
f[x] = 1LL * s * h[cnt] % mod;
g[x] = add(f[x], 1LL * cnt * h[cnt - 1] % mod * s % mod);
}
}
void solve() {
scanf("%d%d", &n, &m);
memset(tag, 0, sizeof(int) * (n + 1));
memset(vis, 0, sizeof(int) * (n + 1));
memset(fir, 0, sizeof(int) * (n + 1));
E = 1;
for (int i = 1; i <= m; ++i) {
int x, y;
scanf("%d%d", &x, &y);
Add_Edge(x, y);
Add_Edge(y, x);
}
if (dfs(1, 0) < 0) return (void) (puts("0"));
memset(vis, 0, sizeof(int) * (n + 1));
int ans = 1;
for (int i = 1; i <= n; ++i) {
if (vis[i]) continue;
dfs2(i);
ans = 1LL * ans * f[i] % mod;
}
printf("%d\n", ans);
}
int main() {
int T;
cin >> T;
h[0] = h[1] = 1;
for (int i = 2; i < N; ++i) {
h[i] = add(h[i - 1], 1LL * (i - 1) * h[i - 2] % mod);
}
while (T--) solve();
return 0;
}
[ZJOI 2012] 波浪
垃圾出题人卡我空间卡我常数卡我题意卡我精度。
虽然题目本身不错但是输出 $ 30 $ 位小数不取模是几个意思?
题意
阿米巴和小强是好朋友。
阿米巴和小强在大海旁边看海水的波涛。小强第一次面对如此汹涌的海潮,他兴奋地叫个不停。而阿米巴则很淡定,他回想起曾经的那些日子,事业的起伏,情感的挫折……总之今天的风浪和曾经经历的那些风雨比起来,简直什么都不算。
于是,这对好朋友不可避免地产生了分歧。为了论证自己的观点,小强建立了一个模型。他海面抽象成一个 $ 1 $ 到 $ N $ 的排列 $ P[1…N] $ 。定义波动强度等于相邻两项的差的绝对值的和,即:
$ L = abs( P_2 – P_1 ) + abs( P_3 – P_2 ) + … + abs( P_N – P_{N - 1} ) $ 给你一个 $ N $ 和 $ M $ ,问:随机一个 $ 1…N $ 的排列,它的波动强度不小于 $ M $ 的概率有多大?
答案请保留小数点后 $ K $ 位输出,四舍五入。
数据范围:当 $ n≤50 $ 时, $ k≤30 $ ;当 $ n≤100 $ 时, $ k≤8 $
题解
考虑拆掉那个绝对值。这只需要从小到大依次加入每个数,每次加入时确定这个数旁边有几个数已经被加入计算贡献即可(例如加入 $ 5 $ 时,它左边已经有数就会产生 $ +5 $ 的贡献,右边还没有数就会产生 $ -5 $ 的贡献)。
用 $ f_{i,j,k,l} $ 表示已经加入了前 $ i $ 个数,加入的数形成了 $ j $ 个连续段,当前产生的贡献是 $ k $ ,左右边界中有 $ l $ 个已经填了数。
需要注意的是,除了那些顶到左右边界的段,其它段我们只关心它们的相对位置。也就是说段是可以滑动的。
- 加入一个数时,有这样几种情况要考虑:
- 加入的数自成一段,且没有顶到左右边界,方案有 $ (j+1-l) $ 种。因为这个新段不能插入左端点左边或右端点右边。
- 加入的数自成一段,且顶到左右边界中的一个,方案有 $ (2-l) $ 种.
- 加入的数在某一段的左右端点,且没有顶到左右边界,方案有 $ (2j-l) $ 种。
- 加入的数在某一段左右端点,且顶到左右边界,方案有 $ (2-l) $ 种。
- 加入的数链接了相邻的两段,方案有 $ (j-1) $ 种
因为出题人的毒瘤,在 $ k=8 $ 时要用 $ long ; double $ 才能不被卡常,在 $ k=30 $ 时要用 $ float128 $ 才能不被卡精。
#pragma GCC optimize("2,Ofast,inline")
#pragma GCC optimize("-ffast-math")
#include<bits/stdc++.h>
#define R register
using namespace std;
const int N = 101;
const int S = 5005;
int n, m, k;
long double g[2][N][S * 2][3];
__float128 f[2][N][S * 2][3];
void write(__float128 x) {
int a[100];
a[0] = (int) x;
for (int i = 1; i <= k + 1; ++i) {
x -= (int) x;
x *= 10;
a[i] = (int) x;
}
if (a[k + 1] >= 5) ++a[k];
for (int i = k; i >= 1; --i) {
if (a[i] == 10) {
a[i] = 0;
++a[i - 1];
}
}
printf("%d", a[0]);
putchar('.');
for (int i = 1; i <= k; ++i) printf("%d", a[i]);
puts("");
}
template <typename T> void solve(T f[2][N][S * 2][3]) {
int now = 1;
f[0][0][S][0] = 1;
R T v;
for (int i = 1; i <= n; ++i) {
memset(f[now], 0, sizeof f[now]);
for (int j = 0; j <= n; ++j) {
for (R int k = 0; k < 2 * S; ++k) {
for (R int l = 0; l < 3; ++l) {
v = f[now ^ 1][j][k][l];
if (v == 0) continue;
f[now][j + 1][k - i * 2][l] += v * (j + 1 - l);
if (l < 2) {
f[now][j + 1][k - i][l + 1] += v * (2 - l);
f[now][j][k + i][l + 1] += v * (2 - l);
}
if (j) {
f[now][j][k][l] += v * (j * 2 - l);
f[now][j - 1][k + i * 2][l] += v * (j - 1);
}
}
}
}
now ^= 1;
}
__float128 ans = 0;
for (int i = m + S; i < 2 * S; ++i) {
ans += f[now ^ 1][1][i][2];
}
for (int i = 1; i <= n; ++i) ans /= i;
write(ans);
}
int main() {
scanf("%d%d%d", &n, &m, &k);
if (n == 1) {
write(1);
return 0;
}
if (m >= n * (n + 1) / 2) {
write(0);
return 0;
}
if (k <= 8) solve(g);
else solve(f);
return 0;
}
[ZJOI 2013] 蚂蚁寻路
题意
给定一个 $ n×m $ 的带权网格。
要求在网格中选出一个封闭图形。具体可以用一条路线描述。
开始位置在某一格点上,方向朝向北。接着右转两次,左转两次,右转两次,左转两次…… 最后再右转一次。规定这条路线上,不能连续旋转两次,不能经过重复的点,最后需要回到起始点,且左转的次数除以 $ 2 $ 等于 $ k $ 。
求所有能选出的图形中,点权和最大的是多少。
$ 1≤n,m≤100,0≤k≤10,abs(A_{i,j})≤10^4 $
题解
可以发现,原问题相当于选出 $ 2k+1 $ 个矩形,这些矩形的底边在一条直线上,且高度高低起伏,最大化他们的权值和。
枚举一个底边 $ down $ ,设 $ f_{i,j,l} $ 表示做到第 $ i $ 列,当前矩形高度为 $ j $ ,用了 $ l $ 个矩形的方案数。下一个矩形应变高还是变低可以通过 $ l $ 的奇偶性确定。
设 $ sum_{x1,y1,x2,y2} $ 表示左上角在 $ (x1,y1) $ ,右下角在 $ (x2,y2) $ 矩形的价值,只考虑变高的情况,一个直观的转移是:
$ f_{i,j,l} = \max_{x≤i-1,y>j} {f_{x,y,l-1} + sum_{j,x,down,i}} $
然而复杂的转移是阻碍优化的根源。如果我们把矩阵数量这一维的贡献算在这个矩阵的第一列上,可以得到更简洁的转移方程:
$ f_{i,j,l} = \max{f_{i-1,j,l},\max_{j‘ > j} {f_{i-1,j,l-1}}} + sum_{j,i,down,i} $
这显然可以前缀和优化,得到 $ O(n^3k) $ 的优秀算法(这里认为 $ n,m $ 同阶)
#include<bits/stdc++.h>
#define R register
using namespace std;
const int N = 105;
const int inf = 0x3f3f3f3f;
int n, m, k;
int a[N][N], sum[N][N];
int f[N][N][25], g[N][N][25], h[N][N][25];
void upd(int &x, int y) {
x = max(x, y);
}
int solve(int n) {
for (int i = 0; i <= m; ++i) {
for (int j = 0; j <= n + 1; ++j) {
for (int l = 0; l <= k; ++l) {
f[i][j][l] = -inf;
g[i][j][l] = -inf;
h[i][j][l] = -inf;
}
}
f[i][n + 1][0] = 0;
}
for (int i = 0; i < m; ++i) {
for (int l = 0; l <= k; ++l) {
g[i][0][l] = -inf;
for (int j = 1; j <= n; ++j) {
upd(g[i][j][l], g[i][j - 1][l]);
upd(g[i][j][l], f[i][j - 1][l]);
}
}
for (int l = 0; l <= k; ++l) {
h[i][n + 1][l] = -inf;
for (int j = n; j >= 1; --j) {
upd(h[i][j][l], h[i][j + 1][l]);
upd(h[i][j][l], f[i][j + 1][l]);
}
}
for (int j = 1; j <= n; ++j) {
for (int l = 1; l <= k; ++l) {
f[i + 1][j][l] = f[i][j][l];
if (l & 1) upd(f[i + 1][j][l], h[i][j][l - 1]);
else if (~l & 1) upd(f[i + 1][j][l], g[i][j][l - 1]);
f[i + 1][j][l] += sum[j][i + 1] - sum[n + 1][i + 1];
}
}
}
int ans = -inf;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
upd(ans, f[i][j][k]);
}
}
return ans;
}
int main() {
scanf("%d%d%d", &n, &m, &k);
k = k * 2 + 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
scanf("%d", &a[i][j]);
}
}
for (int i = 1; i <= m; ++i) {
for (int j = n; j >= 1; --j) {
sum[j][i] = sum[j + 1][i] + a[j][i];
}
}
int ans = -inf;
for (int i = 2; i <= n; ++i) ans = max(ans, solve(i));
cout << ans << endl;
return 0;
}
[ZJOI2013]话旧
题意
有一个长度为 $ n+1 $ 的序列 $ A_0,A_1,...,A_n $ ,满足下列条件。
- $ A_0=A_n=0 $ 。
- 对于 $ 0≤i≤n?1 $ , $ abs(A_i?A_{i+1})=1 $ 。
- 对于所有极小值点 $ k $ ,有 $ A_k=0 $ 。
我们还规定了 $ A $ 的 $ m $ 个位置的值。
求满足条件的序列个数 和 序列最大值的最大值。第一问的答案对 $ 19940417 $ 取模。
数据范围: $ 1≤n≤10^9,0≤k≤10^6 $
题解
思维难度不是很高,主要在于分讨非常多。
设 $ f_{i,0/1} $ 表示到第 $ i $ 个关键点,最后一次是向下/向上的方案数; $ mx_{i,0/1} $ 表示到第 $ i $ 个关键点,最后一次是向下/向上的最大值。
转移就不想写了。看看代码就好了。要特别注意关键点纵坐标是 $ 0 $ 的情况;计算 $ mx $ 时,有些可以在坐标轴上平移直线来简化计算(大不了就是解 $ O(1) $ 个二元一次方程)
#include<bits/stdc++.h>
#define pii pair<int, int>
#define R register
using namespace std;
const int N = 1e6 + 10;
const int mod = 19940417;
int n, m;
int f[N][2], mx[N][2];
pii a[N];
inline void upd(int &x, int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
inline void chmax(int &x, int y) {
x = max(x, y);
}
int Qpow(int x, int p) {
if (p < 0) return 0;
int ans = 1;
while (p) {
if (p & 1) ans = 1LL * ans * x % mod;
x = 1LL * x * x % mod;
p >>= 1;
}
return ans;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%d", &a[i].first, &a[i].second);
}
a[++m] = make_pair(0, 0);
a[++m] = make_pair(n, 0);
sort(a + 1, a + m + 1);
m = unique(a + 1, a + m + 1) - a - 1;
f[1][1] = 1;
for (int i = 2; i <= m; ++i) {
int x0 = a[i - 1].first + a[i - 1].second;
int x1 = a[i].first - a[i].second;
int d = (x1 - x0) / 2;
int ds = a[i].first - a[i - 1].first;
int dh = a[i].second - a[i - 1].second;
if (a[i].second == 0) {
if (a[i - 1].second == 0) {
upd(f[i][1], 1LL * f[i - 1][1] * Qpow(2, d - 1) % mod);
chmax(mx[i][1], mx[i - 1][1]);
chmax(mx[i][1], d);
}
else {
if (d == 0) {
upd(f[i][1], f[i - 1][0]);
chmax(mx[i][1], mx[i - 1][0]);
}
else {
upd(f[i][1], 1LL * f[i - 1][0] * Qpow(2, d - 1) % mod);
chmax(mx[i][1], mx[i - 1][0]);
chmax(mx[i][1], d);
}
upd(f[i][1], 1LL * f[i - 1][1] * Qpow(2, d) % mod);
chmax(mx[i][1], mx[i - 1][1]);
chmax(mx[i][1], a[i - 1].second + d);
}
}
else {
if (a[i - 1].second == 0) {
if (d == 0) {
upd(f[i][1], f[i - 1][1]);
chmax(mx[i][1], mx[i - 1][1]);
}
else {
upd(f[i][1], 1LL * f[i - 1][1] * Qpow(2, d - 1) % mod);
chmax(mx[i][1], mx[i - 1][1]);
chmax(mx[i][1], a[i - 1].second + d);
}
upd(f[i][0], 1LL * f[i - 1][1] * Qpow(2, d - 1) % mod);
chmax(mx[i][0], mx[i - 1][1]);
chmax(mx[i][0], a[i - 1].second + (ds + dh) / 2);
}
else {
if (a[i - 1].second - a[i - 1].first == a[i].second - a[i].first) {
upd(f[i][1], f[i - 1][1]);
chmax(mx[i][1], mx[i - 1][1]);
chmax(mx[i][1], a[i].second);
}
else {
upd(f[i][0], f[i - 1][1]);
chmax(mx[i][0], mx[i - 1][1]);
chmax(mx[i][0], a[i - 1].second + (ds + dh) / 2);
}
if (a[i - 1].second - a[i].first == a[i].second - a[i - 1].first) {
upd(f[i][0], f[i - 1][0]);
chmax(mx[i][0], mx[i - 1][0]);
}
if (d >= 0) {
upd(f[i][1], 1LL * f[i - 1][1] * Qpow(2, d) % mod);
chmax(mx[i][1], mx[i - 1][1]);
chmax(mx[i][1], a[i - 1].second + d);
upd(f[i][0], 1LL * f[i - 1][1] * (Qpow(2, d) - 1) % mod);
if (d > 0) {
chmax(mx[i][0], mx[i - 1][1]);
chmax(mx[i][0], a[i - 1].second + (ds + dh) / 2);
}
if (d == 0) upd(f[i][1], f[i - 1][0]);
else upd(f[i][1], 1LL * f[i - 1][0] * Qpow(2, d - 1) % mod);
chmax(mx[i][1], mx[i - 1][0]);
chmax(mx[i][1], d);
upd(f[i][0], 1LL * f[i - 1][0] * Qpow(2, d - 1) % mod);
chmax(mx[i][0], mx[i - 1][0]);
chmax(mx[i][0], (a[i].second + a[i].first - x0) / 2);
}
}
}
}
cout << (f[m][1] % mod + mod) % mod << ' ' << mx[m][1] << endl;
return 0;
}
[NOI 2015] 寿司晚宴
题意
将 $ 2 $ 至 $ n $ 的正整数划分为三个集合 $ A $ , $ B $ , $ C $ ,使得 $ \forall a \in A, b \in B, gcd(a,b) = 1 $ ,求方案数,对一个读入的 $ p $ 取模输出。
数据范围: $ n≤500,p≤1e9 $
题解
注意到一个性质:一个数 $ n $ 只会有一个大于等于 $ \sqrt{n} $ 的质因子,而在本题中不大于 $ \sqrt n $ 的质因子不超过 $ 8 $ 个。
一个思路是暴力搜出这 $ 8 $ 个质因子归属于谁,然后对于大于含有一个 $ \sqrt n $ 的质因子的数根据这个质因子分类计算。但是我操作一下之后总是会有重复计算的情况。
考虑状压 $ DP $ 。如果只有 $ ≤22 $ 的数(即最大质因子为那 $ 8 $ 个),可以 $ f_{i,j,k} $ 表示考虑到前 $ i $ 个数, $ A $ 集合中包含的质因子集合为 $ j $ , $ B $ 集合中包含质因子集合为 $ k $ 的方案数。显然这里要求 $ j \cap k = \complement $
对于含有至少一个 $ ≥23 $ 的质因子的数,我们把它们按照这个质因子排序,这个质因子相同的一起处理。设 $ g_{0/1,i,j,k} $ 表示把这个质因子分给 $ A/B $ 集合, $ A $ 集合中包含的质因子集合为 $ j $ , $ B $ 集合中包含质因子集合为 $ k $ 的方案数。显然这里要求 $ j \cap k = \complement $ 。
但是两个数组都会包含 $ A,B $ 均不包含该质因子的情况,设这段数对应的区间是 $ [l,r] $ ,那么 $ f_{r,j,k} = g_{0,r,j,k}+g_{1,r,j,k}-f_{l-1,j,k} $
总复杂度可以做到 $ O(n * 3^8) $ ,但这里写的是 $ O(n * 4^8) $
#include<bits/stdc++.h>
using namespace std;
const int N = 505;
const int pri[] = {2, 3, 5, 7, 11, 13, 17, 19};
int n, mod;
int f[N][N], g[2][N][N];
struct number {
int x, p, s;
bool operator < (const number b) const {
if (p != b.p) return p < b.p;
else return x < b.x;
}
} a[N];
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
int main() {
cin >> n >> mod;
for (int i = 1; i < n; ++i) {
a[i].x = i + 1;
int y = i + 1;
for (int j = 0; j < 8; ++j) {
if (y % pri[j] == 0) a[i].s |= (1 << j);
while (y % pri[j] == 0) y /= pri[j];
}
if (y > 1) a[i].p = y;
}
sort(a + 1, a + n);
f[0][0] = 1;
for (int i = 1; i < n;) {
if (a[i].p == 0) {
int s = a[i].s;
for (int j = 255; j >= 0; --j) {
for (int k = 255; k >= 0; --k) {
if (j & k) continue;
if ((s & k) == 0) {
upd(f[j | s][k], f[j][k]);
}
if ((s & j) == 0) {
upd(f[j][k | s], f[j][k]);
}
}
}
++i;
}
else {
for (int j = 0; j < 256; ++j) {
for (int k = 0; k < 256; ++k) {
g[0][j][k] = f[j][k];
g[1][j][k] = f[j][k];
}
}
int nowp = a[i].p;
while (a[i].p == nowp) {
int s = a[i].s;
for (int j = 255; j >= 0; --j) {
for (int k = 255; k >= 0; --k) {
if ((s & k) == 0) {
upd(g[0][j | s][k], g[0][j][k]);
}
if ((s & j) == 0) {
upd(g[1][j][k | s], g[1][j][k]);
}
}
}
++i;
}
for (int j = 0; j < 256; ++j) {
for (int k = 0; k < 256; ++k) {
f[j][k] = (g[0][j][k] + g[1][j][k] - f[j][k]);
if (f[j][k] >= mod) f[j][k] -= mod;
if (f[j][k] < 0) f[j][k] += mod;
}
}
}
}
int ans = 0;
for (int i = 0; i < 256; ++i) {
for (int j = 0; j < 256; ++j) {
upd(ans, f[i][j]);
}
}
cout << ans << endl;
return 0;
}
原文地址:https://www.cnblogs.com/Vexoben/p/11728938.html