动态规划杂记 25.7.29始

动态规划的手记

我真的是 DP 困难户······

0 概率与期望

0.1 洛谷P5104 红包发红包

这个抢红包系统是这样的:假如现在有 \(w\) 元,那么你抢红包能抢到的钱就是 \([0,w]\) 等概率均匀随机出的一个实数 \(x\)

现在红包发了一个 \(W\) 元的红包,有 \(n\) 个人来抢。那么请问第 \(k\) 个人期望抢到多少钱?

输出答案对 \(10^9+7\) 取模后的结果。

显然期望每个人抢到的钱为 \(\dfrac{w}{2}\),所以第 \(k\) 个人抢到 \(\dfrac{W}{2^k}\) 的钱。

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+7;
int n,m,k;
int qpow(int a,int b){
	if(b==1) return a%MOD;
	int s = qpow(a,b/2);
	return b&1?s*s%MOD*a%MOD:s*s%MOD;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>k;
	cout<<n%MOD*qpow(qpow(2,k),MOD-2)%MOD;
	return 0;
}

0.2 洛谷P1850 [NOIP 2016 提高组] 换教室

对于刚上大学的牛牛来说,他面临的第一个问题是如何根据实际情况申请合适的课程。

在可以选择的课程中,有 \(2n\) 节课程安排在 \(n\) 个时间段上。在第 \(i\)\(1 \leq i \leq n\))个时间段上,两节内容相同的课程同时在不同的地点进行,其中,牛牛预先被安排在教室 \(c_i\) 上课,而另一节课程在教室 \(d_i\) 进行。

在不提交任何申请的情况下,学生们需要按时间段的顺序依次完成所有的 \(n\) 节安排好的课程。如果学生想更换第 \(i\) 节课程的教室,则需要提出申请。若申请通过,学生就可以在第 \(i\) 个时间段去教室 \(d_i\) 上课,否则仍然在教室 \(c_i\) 上课。

由于更换教室的需求太多,申请不一定能获得通过。通过计算,牛牛发现申请更换第 \(i\) 节课程的教室时,申请被通过的概率是一个已知的实数 \(k_i\),并且对于不同课程的申请,被通过的概率是互相独立的。

学校规定,所有的申请只能在学期开始前一次性提交,并且每个人只能选择至多 \(m\) 节课程进行申请。这意味着牛牛必须一次性决定是否申请更换每节课的教室,而不能根据某些课程的申请结果来决定其他课程是否申请;牛牛可以申请自己最希望更换教室的 \(m\) 门课程,也可以不用完这 \(m\) 个申请的机会,甚至可以一门课程都不申请。

因为不同的课程可能会被安排在不同的教室进行,所以牛牛需要利用课间时间从一间教室赶到另一间教室。

牛牛所在的大学有 \(v\) 个教室,有 \(e\) 条道路。每条道路连接两间教室,并且是可以双向通行的。由于道路的长度和拥堵程度不同,通过不同的道路耗费的体力可能会有所不同。 当第 \(i\)\(1 \leq i \leq n-1\))节课结束后,牛牛就会从这节课的教室出发,选择一条耗费体力最少的路径前往下一节课的教室。

现在牛牛想知道,申请哪几门课程可以使他因在教室间移动耗费的体力值的总和的期望值最小,请你帮他求出这个最小值。

语文神题。

我们令 \(dp\) 方程 \(dp_{i,j,k}\) 表示上了第 \(i\) 门课,到目前为止申请了 \(j\) 门,当前是否申请 \(0 \textbf{ or } 1\)

我们可以用 \(\textbf{Floyd}\) 算法求出两两教室间的最短路,设为 \(g[i][j]\)

于是有转移方程:

\[dp[i][j][0] = \min \begin{cases} dp[i-1][j][0] + g[c[i-1]][c[i]] \\ dp[i-1][j][1] + g[c[i-1]][c[i]] \times (1-k[i]) + g[d[i-1]][c[i]] \times k[i] \end{cases} \]

\[dp[i][j][1] = \min \begin{cases} dp[i-1][j-1][0] + g[c[i-1]][c[i]] \times (1-k[i]) + g[c[i-1]][d[i]] \times k[i] \\ dp[i-1][j-1][1] + g[d[i-1]][d[i]] \times k[i-1] \times k[i] + g[d[i-1]][c[i]] \times k[i-1] \times (1-k[i]) + g[c[i-1]][d[i]] \times (1-k[i-1]) \times k[i] + g[c[i-1]][c[i]] \times (1-k[i-1]) \times (1-k[i]) \end{cases} \]

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
using namespace std;
const int N = 2e3 + 100;
const double INF = 1e17 + 5;
int n, m, v, e, c[N], d[N];
double k[N], dp[N][N][2];
int g[N][N];
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	memset(g, 0x3f, sizeof(g));
	cin >> n >> m >> v >> e;
	for (int i = 1; i <= n; i++) cin >> c[i];
	for (int i = 1; i <= n; i++) cin >> d[i];
	for (int i = 1; i <= n; i++) cin >> k[i];
	for (int i = 1; i <= e; i++) {
		int a, b, w;
		cin >> a >> b >> w;
		g[a][b] = g[b][a] = min(g[a][b], w);
	}
	for (int k = 1; k <= v; k++) {
		for (int i = 1; i <= v; i++) {
			for (int j = 1; j <= v; j++) {
				g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
			}
		}
	}
	for (int i = 1; i <= v; i++) g[i][i] = 0;
	for (int i = 0; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			dp[i][j][0] = dp[i][j][1] = INF;
		}
	}
	dp[1][0][0] = dp[1][1][1] = 0;
	for (int i = 2; i <= n; i++) {
		dp[i][0][0] = dp[i - 1][0][0] + g[c[i - 1]][c[i]];
		for (int j = 1; j <= min(i, m); j++) {
			int Case1 = c[i - 1], Case2 = d[i - 1], Case3 = c[i], Case4 = d[i];
			dp[i][j][0] = min({ dp[i][j][0],dp[i - 1][j][0] + g[Case1][Case3],dp[i - 1][j][1] + g[Case1][Case3] * (1 - k[i - 1]) + g[Case2][Case3] * k[i - 1] });
			dp[i][j][1] = min({ dp[i][j][1],dp[i - 1][j - 1][0] + g[Case1][Case3] * (1 - k[i]) + g[Case1][Case4] * k[i] });
			dp[i][j][1] = min({ dp[i][j][1],dp[i - 1][j - 1][1] + g[Case2][Case4] * k[i] * k[i - 1] + g[Case2][Case3] * k[i - 1] * (1 - k[i]) + g[Case1][Case4] * (1 - k[i - 1]) * k[i] + g[Case1][Case3] * (1 - k[i - 1]) * (1 - k[i]) });
		}
	}
	double ans = INF;
	for (int i = 0; i <= m; i++) ans = min({ ans,dp[n][i][0],dp[n][i][1] });
	cout << fixed << setprecision(2) << ans;
	return 0;
}
 

0.3 洛谷P5249 [LnOI2019] 加特林轮盘赌

加特林轮盘赌是一个养生游戏。

与俄罗斯轮盘赌等手枪的赌博不同的是,加特林轮盘赌的赌具是加特林。

加特林轮盘赌的规则很简单:在加特林的部分弹夹中填充子弹。游戏的参加者坐在一个圆桌上,轮流把加特林对着自己的头,扣动扳机一秒钟。中枪的自动退出,坚持到最后的就是胜利者。

我们使用的是 2019 年最新技术的加特林,他的特点是无需预热、子弹无限,每一个人,在每一回合,中枪的概率是完全相同的 \(P_0\)

每局游戏共有 \(n\) 只长脖子鹿,从 1 长脖子鹿开始,按照编号顺序从小到大进行游戏,绕着圆桌不断循环。

游戏可能会循环进行多轮,直到场上仅剩下最后一只长脖子鹿时,游戏结束。

给出 \(P_0\)\(n\),询问 \(k\) 号长脖子鹿最终成为唯一幸存者的概率 \(P_k\)

如果 \(P_0=0\),我们认为胜者为 \(1\) 号。

考虑 \(F_{i,j}\) 表示在 \(i\) 个人的情况下存活下来的概率。

很显然,因为只能活一个人。

\[\sum_{i=1}^{n} F_{n,i} = 1 \]

我们可以得知,当一个人崩自己时候,有 \(p\) 的概率崩死自己。

相当于所有人全部向前走了一步,并少了一个人。

\[F_{i,j} \to F_{i-1,j-1} \]

但是 \(1-p\) 的概率没崩死自己。

相当于所有人向前走了一步。

\[F_{i,j} \to F_{i,j-1} \]

于是有一个优雅的转移方程:

\[F_{i,j} = p \times F_{i-1,j-1} + (1-p) \times F_{i,j-1} \]

但是初始状态我们并不知道,即 \(F_{i,1}\) 的情况。

\(F_{1,1} = 1\),显然。

注意到转移方程可以贡献 \(n-1\) 个方程,加上开头提出的公式。

\(n\) 元一次方程,简单解出。

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 100;
double p, dp[2][N];
int n, k, last, cur = 1;
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> p >> n >> k;
	if (p == 0) {
		if (k == 1) cout << 1;
		else cout << 0;
		return 0;
	}
	dp[1][1] = 1;
	for (int i = 2; i <= n; i++) {
		last = cur, cur ^= 1;
		double basea = 1, basec = 0, A = 0, C = 0;
		for (int j = 2; j <= i; j++) {
			basea *= 1 - p;
			A += basea;
			basec = (1 - p) * basec + p * dp[last][j - 1];
			C += basec;
		}
		dp[cur][1] = (1 - C) / (A + 1);
		for (int j = 2; j <= i; j++) {
			dp[cur][j] = p * dp[last][j - 1] + (1 - p) * dp[cur][j - 1];
		}
	}
	cout << fixed << setprecision(10) << dp[cur][k];
	return 0;
}

0.4 洛谷P1654 OSU!

osu 是一款群众喜闻乐见的休闲软件。

我们可以把 osu 的规则简化与改编成以下的样子:

一共有 \(n\) 次操作,每次操作只有成功与失败之分,成功对应 \(1\),失败对应 \(0\)\(n\) 次操作对应为 \(1\) 个长度为 \(n\) 的 01 串。在这个串中连续的 \(X\)\(1\) 可以贡献 \(X^3\) 的分数,这 \(x\)\(1\) 不能被其他连续的 \(1\) 所包含(也就是极长的一串 \(1\),具体见样例解释)

现在给出 \(n\),以及每个操作的成功率,请你输出期望分数,输出四舍五入后保留 \(1\) 位小数。

对于一段长为 \(x\)\(1\),其贡献 \(x^3\)

在其后增加一位,\((x+1)^3 = x^3 + 3x^2 + 3x + 1\)

增加的贡献 \(3x^2 + 3x + 1\)

其中 \(x\) 表示的是当前连续的 \(1\) 的个数的期望,\(x^2\) 表示从当前连续的 \(1\) 产生 \(x^2\) 贡献的期望。

考虑维护这增加段的期望。

因为期望是线性的,但 \(X^3\) 一定不是,所以我们将其拆开单独维护。

因此,\(x1[i] = (x1[i-1] + 1) \times p[i],x2 = (x2[i-1] + 2 \times x1[i-1] + 1)\times p[i]\)

注意到 \(ans[i] = ans[i-1] + (3 \times x2[i-1] + 3 \times x1[i-1] + 1) \times p[i]\),该方程并不相同,因为这是所有的情况,我们仅需要当前期望 \(+\) 增加期望就行了。

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;
int n,m;
double dp[N],a[N],b[N],p[N];
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>p[i];
		a[i] = (a[i-1] + 1)*p[i];
		b[i] = (b[i-1] + 2*a[i-1] + 1)*p[i];
		dp[i] = (dp[i-1] + 3*b[i-1] + 3*a[i-1] + 1)*p[i] + dp[i-1]*(1-p[i]);
	}
	cout<<fixed<<setprecision(1)<<dp[n];
	return 0;
}

0.5 洛谷P3830 [SHOI2012] 随机树

题面太古怪了,不给

叶节点的平均深度的期望值其实很好求。

\(dp_{i}\) 为有 \(i\) 个叶节点的树的期望叶节点的平均深度。

考虑多一个叶节点,即在叶节点上扩展一次,节点深度 \([1,i-1]\)

根据连续性随机变量的期望,\(dp_i = dp_{i-1} + \dfrac{i}{2}\)

第一问完。

树深度的期望更加的困难,因为叶节点数无法描述树深度。

有大佬便定义 \(f_{i,j}\) 为有 \(i\) 个叶节点的树大于 \(j\) 的深度的概率。

这个定义非常巧妙,在统计答案的时候便得知。

这样,我们枚举左子树与右子树的节点数,根据容斥原理得到:

\[f_{i,j} = \sum_{k=1}^{i-1} (f_{k,j-1} + f_{i-k,j-1} - f_{k,j-1} \times f_{i-k,j-1}) \]

那么统计答案时怎么办呢?

我们得到了 \(f_{n,j}\) 的概率,但是并没有求到期望。

在此引用 @aoweiyin 大佬的证明:

关于\(E(x)=\sum^{+\infty}_{i=1}P(i\leq x)\)公式证明

——即树深度的数学期望为何等于 \(\sum^{n-1}_{i=1}f[n][i]\)

\(f[i][j]\)表示当一颗树有i个叶子节点,且深度大于等于\(j\)时的概率;
\(p[i][j]\)表示当一颗树有i个叶子节点,且深度等于\(j\)时的概率;

则有\(f[i][j]=p[i][j]+p[i][j+1]+\dots+p[i][n-1]\)

所以有:

\(f[n][1]=p[n][1]+p[n][2]+\dots+p[n][n-1]\)

\(f[n][2]=p[n][2]+p[n][3]+\dots+p[n][n-1]\)

\(f[n][3]=p[n][3]+p[n][4]+\dots+p[n][n-1]\)

\(\dots\)

\(f[n][n-2]=p[n][n-2]+p[n][n-1]\)

\(f[n][n-1]=p[n][n-1]\)

所有等式累加则有:
\(\sum^{n-1}_{i=1}f[n][i]=p[n][1]\times1+p[n][2]\times2+\dots+p[n][n-2]\times(n-2)+p[n][n-1]\times(n-1)\)

可知:
所得等式恰好为期望的定义式

综上树深度的数学期望等于 \(\sum^{n-1}_{i=1}f[n][i]\)

证毕。

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
using namespace std;
const int N = 105;
int n, m;
double dp[N], f[N][N];
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> m >> n;
	if (m == 1) {
		dp[1] = 0;
		for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + (double)2 / i;
		cout << fixed << setprecision(6) << dp[n];
	}
	if (m == 2) {
		for (int i = 1; i <= n; i++) f[i][0] = 1;
		for (int i = 2; i <= n; i++) {
			for (int j = 1; j <= i - 1; j++) {
				for (int k = 1; k <= i - 1; k++) {
					f[i][j] += (f[k][j - 1] + f[i - k][j - 1] - f[k][j - 1] * f[i - k][j - 1]) / (i - 1);
				}
			}
		}
		double ans = 0;
		for (int i = 1; i <= n - 1; i++) {
			ans += f[n][i];
		}
		cout << fixed << setprecision(6) << ans;
	}
	return 0;
}

1 线性动态规划

1.1 CF1781F Bracket Insertion

题面太长,不放。

容易发现,括号序列一共有 \(1 \times 3 \times 5 \times \dots \times (2n-1)\) 种生成方式。

因为是括号序列,所以可以想到前缀和。

插入 \((\ )\) 相当于将前缀 \(x\) 变为 \(x,x+1,x\),插入 \()\ (\) 相当于将前缀变为 \(x,x-1,x\)

问题转化为:给定一个集合,初始为 \(\{0\}\),每次随机选择一个元素 \(x\),有 \(p\) 的概率将 \(x+1,x\) 加入集合,有 \(1-p\) 的概率将 \(x-1,x\) 加入集合,求最终所有元素非负的概率。

\(f_{k,x}\) 表示初始为 \(x\) 这个数,执行了 \(k\) 次操作仍然合法的方案数,当前枚举的是插入的第一个括号。

我们可以把插入括号左侧,右侧,中间分为三部分计数,可以转化为子问题。

于是有:

\[f_{k,x} = \sum_{i=0}^{k-1}{\sum_{j=0}^{k-i-1}{p\times \binom{k-1}{i}\times\binom{k-i-1}{j}\times f_{i,x}\times f_{j,x+1}\times f_{n-1-i-j,x}}} \\ + \sum_{i=0}^{k-1}{\sum_{j=0}^{k-i-1}{(1-p)\times \binom{k-1}{i}\times\binom{k-i-1}{j}\times f_{i,x}\times f_{j,x-1}\times f_{n-1-i-j,x}}} \]

这样,\(O(n^4)\) 的暴力就打完了。

化简式子是需要技巧的,此题可以直接拆 \(\Sigma\)

\(g(k,x) = \sum_{i=0}^{k}{\dbinom{k}{i} \times f_{i,x} \times f_{k-i,x}}\)

得到 \(f_{k,x} = \sum_{j=0}^{k-1}{(p\times f_{j,x+1}+(1-p)\times f_{j,x-1}) \times \dbinom{k-1}{j} \times g_{k-i,x}}\)

就这样我们得到了 \(O(n^3)\) 的做法。

最后 \(\dfrac{f_{n,0}}{total}\) 就是概率啦~

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505,MOD = 998244353;
int n,q,a,b;
int qpow(int a,int b){
	if(b==0) return 1;
	if(b==1) return a;
	int s = qpow(a,b>>1);
	return b&1?s*s%MOD*a%MOD:s*s%MOD;
}
int inv[N],fac[N];
void init(int n){
	fac[0] = inv[0] = 1;
	for(int i=1;i<=n;i++) fac[i] = fac[i-1]*i%MOD;
	inv[n] = qpow(fac[n],MOD-2);
	for(int i=n-1;i;i--) inv[i] = (i+1)*inv[i+1]%MOD;
}
int Choose(int x,int y){
	return fac[x]*inv[y]%MOD*inv[x-y]%MOD;
}
int dp[N][N],g[N][N];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>q;
	init(n);
	a = q*qpow(10000,MOD-2)%MOD,b = (10000-q)*qpow(10000,MOD-2)%MOD;
	for(int i=0;i<=n;i++) dp[0][i] = g[0][i] = 1;
	for(int i=1;i<=n;i++){
		for(int x=0;x<=n;x++){
			for(int j=0;j<i;j++){
				dp[i][x] = (dp[i][x] + Choose(i-1,j)*(a*dp[j][x+1]%MOD + (x?b*dp[j][x-1]%MOD:0))%MOD*g[i-j-1][x]%MOD)%MOD; 
			}
			for(int j=0;j<=i;j++){
				g[i][x] = (g[i][x] + Choose(i,j)*dp[j][x]%MOD*dp[i-j][x]%MOD)%MOD;
			}
		}
	}
	int tot = 1;
	for(int i=1;i<=2*n;i+=2) tot = tot*i%MOD;
	cout<<dp[n][0]*qpow(tot,MOD-2)%MOD;
	return 0;
}

2 树形动态规划

3 区间动态规划

3.1 CF888F Connecting Vertices

有一个正 \(N\) 边形,顶点顺时针编号为 \(1 \sim N\)。问用 \(N-1\) 条线段连接这 \(N\) 个顶点,使它们连成一棵树,有多少种方法。

连边时有以下限制:

  • 给出一个 \(N\times N\)\(01\) 矩阵 \(A\)\(A_{i,j}\) 表示顶点 \(i\) 和顶点 \(j\) 可以直接相连,而 \(A_{i,j} = 0\) 时则不能。保证了 \(A_{i,i} = 0,A_{i,j} = A_{j,i}\)
  • 两条线段不能在顶点之外的地方相交。

我想到了使用区间 \(DP\) 求解,因为如果线段之间不能相交,枚举 \(l,r\) 时,显然 \([l,r]\) 之间没有连边,所以转移非常简单。

枚举当前边是否相连:

\[f_{l,r} = \begin{cases} \sum_{k=l}^{r-1} f_{l,k}\times f_{k+1,r}\ (A_{i,j} = 1) \\ \sum_{k=l}^{r-1} f_{l,k}\times f_{k,r} \end{cases} \]

结果第二个样例过不去。

因为断点枚举先后顺序不同但确实可能是同一种方案。

所以我们考虑经典的状态扩充:

\(f_{l,r,0/1}\) 表示 \(l,r\) 是否直接相连。

如果 \(l,r\) 是直接相连,那么 \(A_{l,r}=1\),从 \(f_{l,k},f_{k+1,r}\) 转移。

如果 \(l,r\) 是间接相连,那么令 \(k\) 前面没有断点,从 \(f_{l,k},f_{k,r}\) 转移。

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505,MOD = 1e9+7;
int n,m,mp[N][N],dp[N][N][2];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cin>>mp[i][j];
	for(int i=1;i<=n;i++) dp[i][i][0] = 1;
	for(int len=2;len<=n;len++){
		for(int l=1,r=l+len-1;r<=n;l++,r++){
			for(int k=l;k<r;k++){
				if(mp[l][r]){
					dp[l][r][0] = (dp[l][r][0] + (dp[l][k][0]+dp[l][k][1])*(dp[k+1][r][0]+dp[k+1][r][1])%MOD)%MOD;
				}
				if(k>l && mp[l][k]){
					dp[l][r][1] = (dp[l][r][1] + dp[l][k][0]*(dp[k][r][0] + dp[k][r][1])%MOD)%MOD;
				}
			}
		}
	}
	cout<<(dp[1][n][0]+dp[1][n][1])%MOD;
	return 0;
}

4 数位动态规划

5 状压动态规划

5.1 洛谷P2150 [NOI2015] 寿司晚宴

为了庆祝 NOI 的成功开幕,主办方为大家准备了一场寿司晚宴。小 G 和小 W 作为参加 NOI 的选手,也被邀请参加了寿司晚宴。

在晚宴上,主办方为大家提供了 \(n−1\) 种不同的寿司,编号 \(1,2,3,\ldots,n-1\),其中第 \(i\) 种寿司的美味度为 \(i+1\)。(即寿司的美味度为从 \(2\)\(n\)

现在小 G 和小 W 希望每人选一些寿司种类来品尝,他们规定一种品尝方案为不和谐的当且仅当:小 G 品尝的寿司种类中存在一种美味度为 \(x\) 的寿司,小 W 品尝的寿司中存在一种美味度为 \(y\) 的寿司,而 \(x\)\(y\) 不互质。

现在小 G 和小 W 希望统计一共有多少种和谐的品尝寿司的方案(对给定的正整数 \(p\) 取模)。注意一个人可以不吃任何寿司。

因为质因子实在是太少了,可以考虑状压。

\(dp_{i,S_1,S_2}\) 表示枚举到第 \(i\) 个寿司,甲选择后的质因数集合 \(S_1\),乙选择后的 \(S_2\),其中 \(k\) 指当前寿司质因数集合。

\[dp_{i,S_1 | k,S_2} += dp_{i-1,S_1,S_2} (k \& S_2 = 0) \\ dp_{i,S_1,S_2 | k} += dp_{i-1,S_1,S_2} (k \& S_1 = 0) \]

滚动数组到着可以将 \(i\) 滚掉。

但是 \(n \le 500\) 时,质数变得更多了。

注意到一个性质:\(n\) 以内的数超过 \(19\) 的质因数最多只有一个(因为 \(23 \times 23\) 大于 \(500\))。

于是我们考虑把这个大质因数单独存起来,然后考虑将这个状压优化。

设置两个数组:\(f_1,f_2\)

对于大质因数,我们进行排序,这样相同的大质因数是一段一段的。

对于每一段,我们将 \(dp\) 数组赋值给 \(f_1,f_2\)

这样,我们钦定甲乙分别选这个大质因数的情况,保证了大质因数的唯一性。

最后在段尾统计总方案数。

\[dp_{S_1,S_2} = f_{1_{S_1,S_2}} + f_{2_{S_1,S_2}} - dp_{S_1,S_2} \]

\(\huge \mathscr{Code}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505,M = 9;
int n,p,dp[1<<M][1<<M],f1[1<<M][1<<M],f2[1<<M][1<<M];
int prime[9] = {0,2,3,5,7,11,13,17,19};
struct node{
	int val,big,S;
	void init(){
		int i,tmp = val;
		big = -1;
		for(int i=1;i<=8;i++){
			if(tmp%prime[i]) continue;
			S |= (1<<(i-1));
			while(tmp%prime[i]==0) tmp /= prime[i];
		}
		if(tmp!=1) big = tmp;
	}
	friend bool operator<(node a,node b) {return a.big<b.big;}
}num[N];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>p;
	for(int i=1;i<n;i++) num[i].val = i + 1,num[i].init();
	sort(num+1,num+n);
	dp[0][0] = 1;
	for(int i=1;i<n;i++){
		if(num[i].big!=num[i-1].big or num[i].big==-1){
			memcpy(f1,dp,sizeof(f1));
			memcpy(f2,dp,sizeof(f2));
		}
		for(int j=(1<<8)-1;~j;j--){
			for(int k=(1<<8)-1;~k;k--){
				if(j&k) continue;
				if((num[i].S&k)==0) f1[j|num[i].S][k] = (f1[j|num[i].S][k] + f1[j][k])%p;
				if((num[i].S&j)==0) f2[j][k|num[i].S] = (f2[j][k|num[i].S] + f2[j][k])%p;
			}
		}
		if(i==n-1 or num[i].big!=num[i+1].big or num[i].big==-1){
			for(int j=0;j<(1<<8);j++){
				for(int k=0;k<(1<<8);k++){
					if(j&k) continue;
					dp[j][k] = ((f1[j][k] + f2[j][k] - dp[j][k])%p + p)%p;
				}
			}
		}
	}
	int ans = 0;
	for(int j=0;j<(1<<8);j++){
		for(int k=0;k<(1<<8);k++){
			if(j&k) continue;
			ans = (ans + dp[j][k])%p;
		}
	}
	cout<<ans;
	return 0;
}

6 动态规划优化

7 背包动态规划

posted @ 2025-07-29 12:03  OrangeRED  阅读(17)  评论(1)    收藏  举报