前置芝士
好像也没啥( ̄▽ ̄)"
(组合数还是要会求的)
作用
统计对象的可能情况一般比较多,通常需要精确的划分和整体性的计算。因此,使用动态规划抽象出问题中的“子结构”和推导的“阶段”,将有助于我们准确而高效地进行求解。
这些问题往往都起源于排列组合中的组合公式 (C_n^k=C_{n-1}^k+C_{n-1}^{k-1})
例题1
划分数
思路
设 (dp[i][j]) 为 (j) 个物品 划分 (i) 次的方案数
可得到: (dp[i][j]=dp[i][j-i]+dp[i-1][j])
意思是将 (j) 个物品分成 (i) 份,有两种情况: (dp[i][j-i]) 每份划分都大于等于 (1) ; (dp[i-1][j]) 存在有一份以上用 (0) 划分
代码
#include<iostream>
#include<cstdio>
using namespace std;
int n,m;
int dp[1200][1200];
int main(){
scanf("%d%d",&n,&m);
dp[0][0]=1;
for(int i=1;i<=m;i++)
for(int j=0;j<=n;j++){
if(j>=i) dp[i][j]=dp[i][j-i]+dp[i-1][j];
else dp[i][j]=dp[i-1][j];
}
printf("%d",dp[m][n]);
return 0;
}
(其实也可以直接拿组合数做, (C_{m-n+1}^n) 就出来了,讲DP是为了思维)
例题2
CF559C Gerald and Giant Chess
思路
先考虑如果没有黑色格子的时候,我们的方案数是 (C_{h+w-2}^{h-1}) ,若再求出左上到右下经过至少一个黑色格子的路线数量,相减就得到了只经过白色格子的路线数。
然后考虑具体做法:把所有黑色格子按照行,列坐标递增的顺排序。并且把左上角和右下角看作黑色格子。设第 (i) 个格子在第 (x_i) 行第 (y_i) 列。设 (F[i]) 表示从左上角走到第 (i) 个黑色格子,途中不经过其他黑色格子的路线数。
这就是状态转移方程了。
代码
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define int long long
using namespace std;
const int N=3e5+50,mod=1e9+7;
int h,w,n,p[N],inv[N],f[N];
struct node{
int x,y;
}a[N];
bool cmp(node a,node b){
return a.x<b.x||(a.x==b.x&&a.y<b.y);
}
int C(int m,int n){
if(m>n) return 0;
return (p[n]*inv[n-m]%mod*inv[m])%mod;
}
inline int fpow(int a,int b){
int res=1;
while(b){
if(b&1) res=(res*a)%mod;
b>>=1;
a=(a*a)%mod;
}
return res%mod;
}
signed main(){
scanf("%lld%lld%lld",&h,&w,&n);
for(int i=1;i<=n;i++)
scanf("%lld%lld",&a[i].x,&a[i].y);
a[n+1].x=h,a[n+1].y=w;
sort(a+1,a+n+2,cmp);
for(int i=0;i<=N-10;i++) p[i]=(i==0)?1:((p[i-1]*i)%mod),inv[i]=fpow(p[i],mod-2);
for(int i=1;i<=n+1;i++)
f[i]=C(a[i].x-1,a[i].x+a[i].y-2);
for(int i=1;i<=n+1;i++)
for(int j=1;j<=i-1;j++){
if(a[i].x<a[j].x||a[i].y<a[j].y) continue;
f[i]-=f[j]*C(a[i].x-a[j].x,a[i].x+a[i].y-a[j].x-a[j].y);
f[i]=(f[i]+mod)%mod;
}
printf("%lld
",(f[n+1]+mod)%mod);
return 0;
}
例题3
POJ 1737 Connected Graph
思路
我们可以考虑求出N个点的无向图总数,减去N个点的不连通无向图数量,就是答案了。
N个点的无向图总数是 (2^{N*(N-1)/2}) 。含义是N个点的无向图最多有 (N*(n-1)/2) 条边,边有两种状态:有或无。
接下来计算N个点的不连通数量。我们可以枚举标号为1的节点所在的联通块包含的节点数k,从2~N这N-1个节点中选出k-1个。显然有 (C_{N-1}^{~k-1}) 种选法。剩余N-k个节点构成任意无向图,有 (2^{(N-k)*(N-k-1)/2}) 种方法。
设 (F[i]) 表示i个节点的无向连通图个数,则状态转移方程是:
注意:因为没有模数,所以需要高精度!!!
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define re register
#define ll long long
using namespace std;
struct Giao{
int num[1001];
Giao(){num[0]=1;}
inline void clear(){
memset(num,0,sizeof(num)),num[0]=1;
}
inline void read(){
string s;cin>>s,num[0]=s.size();
for(re int i=1;i<=num[0];i++) num[i]=s[num[0]-i];
}
inline void print(){
for(re int i=num[0];i>0;i--) putchar(num[i]+48);
}
inline void operator = (string s){
num[0]=s.size();
for(re int i=1;i<=s.size();i++)
num[i]=num[s.size()-i];
}
inline Giao operator + (Giao x){
Giao y;y.clear();re int i;
for(i=1;i<=num[0]||i<=x.num[0];i++){
y.num[i]+=x.num[i]+num[i];
if(y.num[i]>9) ++y.num[i+1],y.num[i]-=10;
}y.num[0]=i;
while(!y.num[y.num[0]]&&y.num[0]>1) --y.num[0];
return y;
}
inline Giao operator - (Giao x){
Giao y;y.clear();re int i;
for(i=1;i<=num[0];i++){
y.num[i]+=num[i]-x.num[i];
if(y.num[i]<0) --y.num[i+1],y.num[i]+=10;
}y.num[0]=i;
while(!y.num[y.num[0]]&&y.num[0]>1) --y.num[0];
return y;
}
inline Giao operator * (Giao x){
Giao y;y.clear();re int i,j,k;
for(i=1;i<=num[0];i++){
k=0;
for(j=1;j<=x.num[0];j++)
y.num[i+j-1]+=num[i]*x.num[j]+k,
k=y.num[i+j-1]/10,y.num[i+j-1]%=10;
y.num[i+x.num[0]]+=k;
}y.num[0]=i+j;
while(!y.num[y.num[0]]&&y.num[0]>1) --y.num[0];
return y;
}
}p2[2501],c[51][51],dp[51];//p2是2的次方 c是组合数 dp就是动态转移方程
int n;
int main(){
p2[0]="1";
for(int i=1;i<=2500;i++) p2[i]=p2[i-1]+p2[i-1];
for(int i=0;i<=50;i++){
c[i][0]="1";
for(int j=1;j<=i;j++)
c[i][j]=c[i-1][j]+c[i-1][j-1];
}
while(scanf("%d",&n),n){
memset(dp,0,sizeof(dp)),dp[1]="1";
for(int i=2;i<=n;i++){
dp[i]=p2[i*(i-1)>>1];
for(int j=1;j<i;j++)
dp[i]=dp[i]-dp[j]*c[i-1][j-1]*p2[(i-j)*(i-j-1)>>1];
}dp[n].print(),putchar('
');
}
return 0;
}
例题4
POJ 1037 A decorative fence
思路
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
int t,n;
ll m,f[21][21][2];
inline void init(){
f[1][1][0]=f[1][1][1]=1;
for(int i=2;i<=20;i++)
for(int j=1;j<=i;j++){
for(int p=j;p<=i-1;p++) f[i][j][0]+=f[i-1][p][1];
for(int p=1;p<=j-1;p++) f[i][j][1]+=f[i-1][p][0];
}
}
int main(){
init();
scanf("%d",&t);
while(t--){
scanf("%d%lld",&n,&m);
bool used[21];
memset(used,0,sizeof(used));
int last,k;
//第1块木板,即可能处于高位,也可能处于高位,也可能处于低位,单独处理
for(int j=1;j<=n;j++){
if(f[n][j][1]>=m){
last=j,k=1;
break;
}
else m-=f[n][j][1];
if(f[n][j][0]>=m){
last=j,k=0;
break;
}
else m-=f[n][j][0];
}
used[last]=1;
printf("%d",last);
//第2~n块木板,高低位置、合法的长度范围与上一块木板有关
for(int i=2;i<=n;i++){
k^=1;
//真实长度为len,在剩余木板中排名为j
int j=0;
for(int len=1;len<=n;len++){
if(used[len]) continue;
j++;
if(k==0&&len<last||k==1&&len>last){
if(f[n-i+1][j][k]>=m){
last=len;
break;
}
else m-=f[n-i+1][j][k];
}
}
used[last]=1;
printf(" %d",last);
}
puts("");
}
return 0;
}