一、计数问题(数位DP)
问题描述:给定两个数字a和b,求这两个数之间的所有数中各个数字出现的次数。
问题分析:题目区间[ a , b ]两端都是未知数字,可以转化为求区间[ 1 , a ]和区间[ 1 , b ],再利用前缀和思想作差,这样就能将问题化简为一端是未知数字。在统计数字 x 在区间[ 1 , n ]中数字出现次数时,可以按出现的位次进行分情况讨论,示例如图。此外还需注意边界问题,按从左往右位序分情况时,由于前导零不占位序,统计0出现的次数时前半部分不能全为0,最小从001开始。
代码:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int get(vector<int> num,int l,int r){//由位序求数字
int res=0;
for(int i=l;i>=r;i--)
res=res*10+num[i];
return res;
}
int power10(int x){//求位序权重
int res=1;
while(x--) res*=10;
return res;
}
int count(int n,int x){
if(!n) return 0;
vector<int> num;
while(n){
num.push_back(n%10);//按位拆解数字
n/=10;
}
n=num.size();
int res=0;
for(int i=n-1-!x;i>=0;i--){//按位遍历,注意x=0时没有求前半部分
if(i<n-1){//求前半部分
res+=get(num,n-1,i+1)*power10(i);
if(!x) res-=power10(i);//x=0时要从001开始
}
if(num[i]==x) res+=get(num,i-1,0)+1;//求后半部分
else if(num[i]>x) res+=power10(i);
}
return res;
}
int main(){
int a,b;
while(cin>>a>>b,a||b){
if(a>b) swap(a,b);
for(int i=0;i<10;i++){
cout<<count(b,i)-count(a-1,i)<<' ';//利用前缀和思想做差
}
cout<<endl;
}
return 0;
}
输出示例:
二、蒙德里安的梦想(状态压缩DP)
题目要求:
题目分析:f[ i , j ]表示第 i 列在 j 情况( j 是一个2进制数,从左往右数第 x 位为1代表第 x 行与前一列一起构成了横向长方形)下可以划分长方形的方案数。从f[ i-1 , j ]转移到f[ i , j ]的划分方案 k 要注意两个条件:① k & j == 0,不为0时代表当前行的 i-1 列已经有了长方形尾,第 i 列不可能出现长方形尾。② j | k 不能出现连续的奇数个0,若出现无法用竖着的长方形填满。
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1010;
int f[N][N];
bool st[N];
int main(){
int n,m;
while(cin>>n>>m,n||m){
memset(f,0,sizeof f);
for(int i=0;i<1<<n;i++){//提取遍历所有k的取值,找出连续奇数个0的方案
st[i]=true;
int cnt=0;//记录连续0的个数
for(int j=0;j<n;j++){//遍历此方案的每一位
if(i>>j&1){//连续的0中断了
if(cnt&1) st[i]=false;//连续0的个数是奇数
cnt=0;
}
else cnt++;
}
if(cnt&1) st[i]=false;//处理最后一段连续的0
}
f[0][0]=1;
for(int i=1;i<=m;i++){//遍历列
for(int j=0;j<1<<n;j++){//遍历j
for(int k=0;k<1<<n;k++){//遍历可能方案K
if((j&k)==0&&st[j|k]){//此方案可行
f[i][j]+=f[i-1][k];
}
}
}
}
cout<<f[m][0]<<endl;
}
return 0;
}
输出示例:
三、最长hamilton路径(状态压缩DP)
问题描述:给定一张n个结点的带权无向图,求起点0到终点n-1的最短hamilton路径。(从0到n-1不重不漏地经过每个点恰好一次。)
问题分析:用f[ i , j ]表示从0到 j 的一条路径 i (二进制数,从右往左数第 x 位为 1 代表经过编号为 x 的点)的最短长度。按照路径倒数第二个点划分 n 种情况,则f[ i , j ] = min ( f[ i-{k} ,k] + a[ k , j ] )。
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=20,M=1<<N;
int f[M][N];
int a[N][N];
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>a[i][j];//存储无向图权重
memset(f,0x3f,sizeof f);//初始化
f[1][0]=0;
for(int i=0;i<1<<n;i++)//遍历所有可能路径
for(int j=0;j<n;j++)//遍历不同终点
if(i>>j&1){//该路径能达到该终点
for(int k=0;k<n;k++){//遍历倒数第二个点
if((i-(1<<j))>>k&1)//该路径能到达k点且k点与j点不为同一点
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+a[k][j]);//找到最短路径
}
}
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
输出示例:
参考资料:acwing