1.数位统计
要分情况讨论!
题目:
算法思路:因为求的是一个区间内的值,所以利用前缀和思想。求出1~n中,x出现的次数即可。
q:如何求出1~n中,x出现的次数?
a:求出x在每一位上出现的次数。
求1~n中,x出现的次数:
例如:n=abcdefg , 求x在第4位出现的次数
分类讨论:
①前三位:000abc-1,x,后三位000999. 方案数:abc*1000
②前三位:abc,x
2.1 d < x , 后三位:无解. 方案数:0
2.2 d = x , 后三位:000~dfg. 方案数:dfg+1
2.3 d > x , 后三位:000~999. 方案数:1000
注意:
1.当判断x在第1位出现的次数时,不存在情况①
2.当x=0且在分类①时,因为不能前导全0,因此得从001开始,(这一步特判即可)
#include <iostream>
#include <vector>
using namespace std;
int get(vector<int> num , int l , int r)//取出num中第l~r位数
{
int res = 0;
for(int i = l ; i >= r ; i--)
res = res * 10 + num[i];
return res;
}
int power10(int x)//返回10的x次
{
int res = 1;
while(x--)
res *= 10;
return res;
}
int count(int n , int x)//1~n中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);//如果找的数是0,则不存在前导全零的情况,要减掉1种状况
}
if(num[i] > x) res += power10(i);
else if(num[i] == x) res += get(num , i - 1 , 0) + 1;
}
return res;
}
int main()
{
int n , m;
while(cin >> n >> m , n || m)
{
if(n > m) swap(n , m);
for(int i = 0 ; i <= 9 ; i++)
cout << count(m , i) - count(n - 1 , i) << ' ';
cout << endl;
}
return 0;
}
2.状压DP
1.题目:求把NM的棋盘分割成若干个12的的长方形,有多少种方案。
思路:
1.所谓状压DP,就是利用二进制保存状态,因为二进制方便位运算。
2.这题等价与找所有横放方块的方案,因为一种横放方块的方案,对应一种竖放方块的方案,也就是一种方案。
3.用f[i][j](列的标号从0开始)表示当前第i列的状态是j。如果j=1说明当前第i列被同行第i-1列伸出来的方块影响到。因此状态转移方程就是本列的每一个状态都是由上一列合法方案转移过来,即f[i][j]=f[i-1][k],其中k是i-1的一种状态。
4.转移时有两个条件:
(1)j|k这个状态的二进制表示中不存在连续奇数的0,如果存在则无法正好放下竖放的方块。
(2)第i列和第i-1列不同时横置方块,即j&k == 0
5.初始化f[0][0]=1,因为第0列只能是0,不被任何方块影响。
6.最后输出f[m][0],表示第0~m-1列已完成,第m列不被方块影响到的方案数,即最终方案数。
#include <iostream>
#include <cstring>
using namespace std;
//方案数 = 横放木块的方案
const int N = 12 , M = 1 << N;
long long f[N][M];//f[i][j]表示当前第i列的状态的是j。如果j=1说明当前第i列被同行第i-1列伸出来的方块影响到。
bool st[M]; //序号从0开始标
int main()
{
int n , m;
while(cin >> n >> m , n|m)
{
for(int i = 0 ; i < 1 << n ;i++)//条件(1)判断i的二进制数是否有连续的奇数个0
{
int cnt = 0;
st[i] = true;
for(int j = 0 ; j < n ; j++)
if(i >> j & 1)//遇到了1
{
if(cnt & 1)//对1的个数的奇偶进行判断
{
st[i] = false;
break;
}
}
else cnt ++;
if(cnt & 1) st[i] = false;
}
memset(f , 0 , sizeof f);
f[0][0] = 1;//第0列状态只能是0,因此只有一种方案
for(int i = 1 ; i <= m ; i++)//第0列已经考虑,所以从第1列开始,之所以计算第m列,是为了最后答案的输出
for(int j = 0 ; j < 1 << n ; j ++)//第i列的状态
for(int k = 0 ; k < 1 << n ; k++)//第i-1列的状态
if(!(j & k) && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
2.题目:给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
算法思路:f[i][j]表示从0走到j,中间经过的点是i表示的二进制数中是1的位置.
以暂时终点的前一个点分类,即引入一个中转点k,用k去更新j。
状态转移方程是f[i][j] = f[i - 1<<j][k]+g[k][j]
#include <iostream>
#include <cstring>
using namespace std;
const int N = 21 , M = 1 << N;
int n;
int g[N][N];
int f[M][N];
int main()
{
cin >> n;
for(int i = 0 ; i < n ; i++)
for(int j = 0 ; j < n ; j++)
cin >> g[i][j];
memset(f , 0x3f , sizeof f);
f[1][0] = 0;
for(int i = 1 ; i < 1 << n ; i++)//枚举每一种经过情况
for(int j = 1 ; j < n ; j++)//枚举终点
if(i >> j & 1)//如果这个终点在i中出现过,则继续,否则无意义
for(int k = 0 ; k < n ; k++)//枚举中转点,k要从0开始!去更新别的点。
if(i >> k & 1)//如果中转点也在i中出现过,继续
f[i][j] = min(f[i][j] , f[i - (1 << j)][k] + g[k][j]);//原来的距离和经过中转点后的距离相比
cout << f[(1 << n) - 1][n - 1] << endl;
return 0;
}
3.树形DP
题目:没有上司的舞会
算法思路:所有人的关系可以建立成一颗树,直接上司即是父亲,父亲出现则儿子不能出现。那么每个人有两种状态:1和0,即出场和不出场。声明一个f[N][2],第一维表示人,第二维表示状态。
分类讨论,记i是父亲,j是儿子
(1)当父亲出场时,儿子必不可能出场,即:f[i][1] += f[j][0]
(2)当父亲不出场时,从儿子出场和不出场中选一个max,即f[i][0] += max(f[j][1],f[j][0])
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6010;
int e[N] , ne[N] , h[N] , idx;
int f[N][N];
bool has_fa[N];
int happy[N];
int n;
int 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 ; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += max(f[j][1] , f[j][0]);
f[u][1] += f[j][0];
}
}
int main()
{
memset(h , -1 , sizeof h);
cin >> n;
for(int i = 1 ; i <= n ; i++) cin >> happy[i];
for(int i = 1 ; i < n ; i++)
{
int a , b;
cin >> b >> a;
add(a , b);
has_fa[b] = true;
}
int root = 1;
while(has_fa[root]) root++;
dfs(root);
cout << max(f[root][1] , f[root][0]) << endl;
}
4.记忆化搜索
题目:滑雪
算法思路:f[i][j]表示从(i,j)出发的最大值。记录从每一个点出发的最大值,然后遍历从每一个点出发的四个方向,并且有两个约束条件:(1)下一个点的高度比原来的点低;(2)点在图内.
用递归来求最大距离
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int h[N][N];
int f[N][N];
int res;
int n , m;
int dx[4] = {-1 , 0 , 1 , 0} , dy[4] = {0 , 1 , 0 , -1};
int dp(int i , int j)
{
int &v = f[i][j];
if(v != -1) return v;
v = 1;
for(int k = 0 ; k < 4 ; k++)
{
int a = dx[k] + i , b = dy[k] + j;
if(a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[i][j])
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);//如果是-1说明没被找过
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 << endl;
return 0;
}