目录
一、数位dp(分情况讨论)
统计数的出现次数:
分析:直接统计数字x在1~n之间出现的次数,然后利用前缀和的性质,即可求得数字x在a~b之间出现的次数。
分情况讨论:以数字abcdefg为例,统计数字x,在第四位(d)上在1~n出现的次数。其情况有如下几种:
- 后三位小于abc,则后三位取值有000~abc-1,前三位数字可取000~999,则情况数为abc*1000
- 后三位等于abc:
- 若d>x:则后三位数字可取000~999
- 若d==x:则后三位数字可取000~efg
- 若d<x:情况不存在
- 若x==0,后三位等于abc的情况与上述一致。
- 当后三位小于abc时,若后三位取000,则出现前导0,表示不合法,不会计算x的出现次数,因此后三位取值是001~abc,即少了1000种情况。
由上述知,对x在第i位,设这个数N有n位:
- 若x在最高位,则只有后面位数等于abc的情况。x是0的话,则不需枚举
- 若x不在最高位
- i+1~n位<原数时,情况数为(i+1~n位-!x)*10^i
- i+1~n位==原数时
- 若第i位数大于x,情况数为(10^i)
- 若第i位数等于x,情况数为(1~i位+1)
把在每一位上出现的次数加起来即得1~N内x出现的次数,利用前缀和性质可算出答案。
代码如下:
//这里填你的代码^^
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
/*
001~abc-1, 999
abc
1. num[i] < x, 0
2. num[i] == x, 0~efg
3. num[i] > x, 0~999
*/
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--)
{
if(i<n-1)
{
res+=get(num,n-1,i+1)*power10(i);
if(!x) res-=power10(i);
}
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)
{
if(a>b) swap(a,b);
for(int i=0;i<=9;i++)
cout<<count(b,i)-count(a-1,i)<<" ";
cout<<endl;
}
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3924723/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
二、状态压缩dp
状态压缩dp通常是将每一种状态转化为二进制数,从而枚举每一个二进制数,从而判断每一种状态。
例题1、蒙德里安的梦想
题目分析:
如果横向的长方形摆放完了,那么纵向的长方形只能一个一个填入棋盘中。因此总方案数等于横向摆放长方形的方案数。
- 状态表示f(i,j):表示前i-1列已经摆放完,第i-1列伸到第i列的状态是j的摆放第i列的方案数。属性为数量。
- 状态计算:集合划分依据为摆放第i-1列时,第i-2列伸出到第i-1列的所有可能状态。
枚举第i列j的所有状态,加上第i-1列能转移到j的状态k的方案数:
状态k能转移到j的判断标准:
- 摆放不冲突,不摆放到已经被占用的位置,即j&k==0。
- 摆放后,连续的0的数量为偶数。
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
const int N =12,M=1<<N;
typedef long long LL;
vector<int> state[M];//预处理可以转移到这个状态的所有状态
bool st[M]; //判断这个状态是否符合连续的0数量是偶数个
int n,m;
LL f[N][M];//前n-1列已经摆好,正在摆放第n列,且n-1列“伸出”到第n列的状态是j
int main()
{
while(cin>>n>>m,n||m)
{
//预处理满足连续0数量偶数个的状态
for(int i=0;i<1<<n;i++)
{
bool is_valid =true;
int cnt=0;//记录连续0数量
for(int j=0;j<n;j++)//n行 位数为n 所以循环n次
if(i>>j &1)//如果该位是1
{
if(cnt&1)//如果是奇数
{
is_valid=false;
break;
}
cnt=0;
}
else cnt++;
if(cnt&1) is_valid=false;//最后剩一段需要额外判断
st[i]=is_valid;
}
for(int i=0;i<1<<n;i++)
{
//存储能转移去 i 的状态
state[i].clear();//把上次用过的清空
for(int j=0;j<1<<n;j++)
if((i&j)==0&&st[i|j])
state[i].push_back(j);
}
memset(f,0,sizeof f);
f[0][0]=1;//一个都不放 方案数为1
//实际上规定棋盘是0到m-1,从1开始遍历意思是第0列已经摆好。
for(int i=1;i<=m;i++)
for(int j=0;j<1<<n;j++)//枚举第m层的所有状态
for(auto k:state[j])
f[i][j]+=f[i-1][k];
cout<<f[m][0]<<endl;
}
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3922502/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
例题2、最短哈密顿距离
题目分析:
- 状态表示f(i,j):移动到第j个点时,走过的情况为i的所有方案,属性为min。
- 状态计算: 集合划分为走到前一个点k时,所有可能的状态s。
- 状态转移方程:
步骤:先遍历所有状态,遍历所有点,找出经过该点i的状态。遍历所有前一个点,找出合法状态(未经过i点,但经过k点),然后利用状态转移方程。
//这里填你的代码^^
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =20,M=1<<N;
int w[N][N];
int f[M][N];
int n;
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(f,0x3f,sizeof f);
f[1][0]=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)
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
cout<<f[(1<<n) -1][n-1];
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3923470/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三、树形dp
题目分析:
- 状态表示f(i,j):表示j为0、1,表示选择或不选择第i个点。集合在以第i个点为根的子树中选择,选或不选第i个点的最大happy值。属性为max
- 状态转移方程
- 如果j为0,则最佳方案中,其子节点有可能选也有可能不选。对所有子节点k
- 如果j为1,则最佳方案中,其子节点肯定不选。对所有子节点k
实现方式:从根节点开始搜,搜索其所有子节点。然后递归搜索子节点的子节点,逐步返回f。
最后找出根节点root,答案即为
代码如下:
//这里填你的代码^^
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =6010;
int happy[N];
int h[N],e[N],ne[N],idx;
int f[N][2];
bool has_father[N]; //找根节点
int n;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
f[u][1]=happy[u];
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
dfs(j);
f[u][0]+=max(f[j][0],f[j][1]);
f[u][1]+=f[j][0];
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>happy[i];
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b;
cin>>a>>b;
has_father[a]=true;
add(b,a);
}
int root=1;
while(has_father[root]) root++;
dfs(root);
cout<<max(f[root][0],f[root][1]);
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3923838/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
四、记忆化搜索
用f[N,N]存储每个点的最长距离,这样在遍历到某个点时,直接加上其f值即可。
实现:搜索某个点时,如果搜索过就直接返回。如果未搜索过,首先f置为1,搜索四周合法点,f为其中最大值加上1。
代码如下 :
//这里填你的代码^^
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =310;
int n,m;
int f[N][N];
int h[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dp(int x,int y)
{
int& v=f[x][y];
if(v!=-1) return v;
v=1;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a >= 1 && a <= n && b >= 1 && b <= m && h[x][y] > h[a][b])//能走通
v=max(v,dp(a,b)+1);
}
return v;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>h[i][j];
memset(f,-1,sizeof f);
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
res=max(res,dp(i,j));
cout<<res;
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3924139/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。