关于一类插入-合并类 dp 的思路
关于一类插入-合并dp的做法
前言
这类问题通常是有很多个小部分,dp 时要考虑其排列,但是我们无法知晓其顺序,而这些部分最后要合并为一个整体。这类问题需要用到这种思路。
例题
题意
给定一个长为
分析
拿到这个题其实还是蛮蒙的,但是如果我们转化(抽象)一下题意,就会发现这道题可以看作:
求以
这里的每个排列,其实就是跳跃顺序。
这样,我们就可以考虑从小到大,向已有的排列中加入数。设
1.连接两段元素
因为是按照从小到大插入,故一定满足插入的数大于两端的数。如果前
2.单独成段,插入原排列
这种情况下,如果原序列有
于是有
3.插入排列中任意一段充当首或尾
我们发现,这样插入之后,必然会在之后把比它大的数放在旁边,这样就无法满足条件了。
代码:
#include<bits/stdc++.h> using namespace std; const int N = 2020, mod = 1e9+7; void add(int &a, int b){ a = (1ll*a+1ll*b)%mod; } int dp[N][N]; int n, s, t; int main(){ scanf("%d%d%d", &n, &s, &t); dp[1][1] = 1;//第一个数分成1段有一种方案。 for(int i = 2; i<=n; ++i){ for(int j = 1; j<=i; ++j){ if((i ^ s)&&(i ^ t)){ add(dp[i][j], 1ll*dp[i-1][j+1]*j%mod); add(dp[i][j], 1ll*dp[i-1][j-1]*(j-(i>s)-(i>t))%mod); } else{ add(dp[i][j], dp[i-1][j-1]); add(dp[i][j], dp[i-1][j]);//特殊处理s和t。 } } } printf("%d\n", dp[n][1]); system("pause"); return 0; }
豌豆射手
题目描述
现在有
每一个豌豆射手都有一个攻击半径
戴夫要把这
输入格式
第一行两个整数
第二行
输出格式
一行一个整数,表示合法的方案数
样例 #1
样例输入 #1
4 4 1 1 1 1
样例输出 #1
24
样例 #2
样例输入 #2
3 47 4 8 9
样例输出 #2
28830
样例 #3
样例输入 #3
8 100000 21 37 23 13 32 22 9 39
样例输出 #3
923016564
数据范围
- 对于
的数据:
- 对于
的数据:
- 对于
的数据
思路
这道题和上一道题有异曲同工之妙。
我们先来考虑,如果我们把豌豆射手的每一个排列都找出来,然后让他们密集地摆放,会发现,这个排列的长度(就是左右两端的豌豆射手之间的距离)应该为
然后我们来考虑如何搞出来对于每一个
我们分类讨论。因为从小到大排好序,所以新加入的豌豆射手一定是可以贡献到长度上的。
- 如果插入的豌豆射手合并到某个块的一端,有
。这里的 是因为有 个块, 是因为两端都可以放。 - 如果插入的豌豆射手合并了原有的两个块,那么就有
。这里的 是因为我们不知道要合并哪两块。 - 如果插入的豌豆射手自成一个新块,有
。
代码:
#include<bits/stdc++.h> using namespace std; const int N = 50, M = 1e5+100, mod = 1e9+7; inline int read(){ int x = 0; char ch = getchar(); while(ch<'0' || ch>'9') ch = getchar(); while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar(); return x; } int f[N][N][M]; int inv[M<<1], jie[M<<1]; inline int fpow(int a, int b){ int ret = 1; a%=mod; while(b){ if(b & 1){ ret = (1ll*ret*a)%mod; } b>>=1; a = (1ll*a*a)%mod; } return ret; } inline int C(int n, int m){ if(n < m) return 0; return 1ll*jie[n]*inv[m]%mod*inv[n-m]%mod; } void add(int &a, int b){ a = (a+b)%mod; } int n, d; int R[N]; int mx; int main(){ n = read(), d = read(); for(int i = 1; i<=n; ++i) R[i] = read(), mx = max(R[i], mx); sort(R+1, R+n+1); jie[0] = 1; for(int i = 1; i<=110000; ++i) jie[i] = 1ll*jie[i-1]*i%mod; inv[110000] = fpow(jie[110000], mod-2); for(int i = 110000-1; i>=0; --i) inv[i] = (1ll*inv[i+1]*(i+1))%mod; f[0][0][0] = 1; for(int i = 1; i<=n; ++i){ for(int j = 1; j<=i; ++j){ for(int k = 1; k<=mx*i; ++k){ if(k>=R[i])add(f[i][j][k], 1ll*2*j*f[i-1][j][k-R[i]]%mod); if(k>=R[i]*2-1)add(f[i][j][k], 1ll*f[i-1][j+1][k-R[i]*2+1]*j%mod*(j+1)%mod); add(f[i][j][k], 1ll*f[i-1][j-1][k-1]); } } } int ans = 0; for(int i = 0; i<=min(n*mx, d); ++i){ add(ans, 1ll*f[n][1][i]*C(n+d-i, n)%mod); } printf("%lld\n", ans); return 0; }
简要题意
给定
首先我们可以观察到,
那么,问题完全可以转化成求最后权值为
具体的讲,分为以下五种情况。
- 在两边插入,单独成块,贡献
,方案 ; - 在两边插入,和边界连通块组成块,贡献
,方案 ,要求 ; - 在中间插入,独立成块,贡献
,方案 ; - 在中间插入,与一个块相连,贡献
,方案 ; - 在中间插入,连接两个块,贡献
, 方案 。
这题一个恶心的点就在于要根据数据范围来考虑采用 long double 还是 __float128,另外一点就是
代码:
```cpp #include<bits/stdc++.h> using namespace std; const int N = 105; int n, m, K; int now; namespace solve1{ long double ans, f[2][N][10010][3];//插入第 i 个数,有 j 个段,价值为 k,用了几个边界 void output(double ret){ if(ret+1e-14 >= 1) { cout << "1." << string(K, '0') << endl; return; } cout << "0."; ret*=10; for(int i = 1; i<=K; ++i){ cout << (int)(ret + (K == i) * 0.5); ret = (ret-(int)ret)*10; } } } namespace solve2{ __float128 ans, f[2][N][10010][3]; void output(__float128 ret){ if(ret+1e-14 >= 1) { cout << "1." << string(K, '0') << endl; return; } cout << "0."; ret*=10; for(int i = 1; i<=K; ++i){ cout << (int)(ret + (K == i) * 0.5); ret = (ret-(int)ret)*10; } } } //在两边插入,单独成块,贡献-i,方案2-p //在两边插入,和边界连通块组成块,贡献i,方案2-p, j>0; //在中间插入,独立成块,贡献-2i,方案j-1+2-p; //在中间插入,与一个块相连,贡献0,方案2*(j-1)+2-p //在中间插入,连接两个块,贡献2i, 方案 j-1 int main(){ scanf("%d%d%d", &n, &m, &K); if(K<=8){ using namespace solve1; // if(m>10099){ // ans = 0; // cout << fixed << setprecision(K) << ans << endl; // return 0; // } f[0][0][5000][0] = 1; for(int i = 1; i<=n; ++i){ // double tmp = 1.0/i; now^=1; memset(f[now], 0, sizeof(f[now])); for(int k = 0; k<=10000; ++k){//后面枚举的都是i-1 for(int j = 0; j<i; ++j){ for(int p = 0; p<=2; ++p){ if(!f[now^1][j][k][p]) continue; if(p<2){ f[now][j+1][k-i][p+1]+=f[now^1][j][k][p]*(2-p)/i; } if(j>0 && p<2){ f[now][j][k+i][p+1]+=f[now^1][j][k][p]*(2-p)/i; } f[now][j+1][k-2*i][p]+=f[now^1][j][k][p]*(j+1-p)/i; if(j>0){ f[now][j][k][p]+=f[now^1][j][k][p]*(2*j-p)/i; } if(j>=2){ f[now][j-1][k+2*i][p]+=f[now^1][j][k][p]*(j-1)/i; } } } } } ans = 0; for(int i = m; i<=5000; ++i){ ans+=f[now][1][i+5000][2]; } output(ans); } else{ using namespace solve2; // if(m>10099){ // ans = 0; // output(ans); // return 0; // } f[0][0][5000][0] = 1; for(int i = 1; i<=n; ++i){ // __float128 tmp = 1.0/i; now^=1; memset(f[now], 0, sizeof(f[now])); for(int k = 0; k<=10000; ++k){//后面枚举的都是i-1 for(int j = 0; j<i; ++j){ for(int p = 0; p<=2; ++p){ if(!f[now^1][j][k][p]) continue; if(p<2){ f[now][j+1][k-i][p+1]+=f[now^1][j][k][p]*(2-p)/i; } if(j>0 && p<2){ f[now][j][k+i][p+1]+=f[now^1][j][k][p]*(2-p)/i; } f[now][j+1][k-2*i][p]+=f[now^1][j][k][p]*(j+1-p)/i; if(j>0){ f[now][j][k][p]+=f[now^1][j][k][p]*(2*j-p)/i; } if(j>=2){ f[now][j-1][k+2*i][p]+=f[now^1][j][k][p]*(j-1)/i; } } } } } ans = 0; for(int i = m; i<=5000; ++i){ ans+=f[now][1][i+5000][2]; } output(ans); } return 0; }
总结
这类 dp 问题的重点是考虑全面,注意细节,然后通过合理调整 dp 顺序来达到降低难度的目的。比较常见的情况分类有:是否作为边界;连接两个块,接在一个块上,还是单独成块。此博客还会随例题的增多而更新。