动态规划技巧
一、0,1背包问题
一、问题描述:
有n 个物品,它们有各自的重量和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
解题思路:对于每一个转态,只有选择和不选择两中选择。
记忆化搜索
int n,capacity;
int volume[100],need[100];
int cache[1001][1001];
string name[100];
//背包剩余空间为capacity 时,将返回放入item以后的物品
//所能达到的最大值
int pack(int capacity,int item){
if(item == n)return 0;
int &ret = cache[capacity][item];
if(ret != -1)return ret;
//不放入物品的情况
ret = pack(capacity,item+1);
//不放入物品的情况
if(capacity>=volume[item])
ret = max(ret,pack(capacity-volume[item],item+1)+need[item]);
return ret;
}
迭代法
dp[i][j] 的含义是在j容量下放入i件物品的最大价值。在遍历容量的时候记得是从后向前遍历的,否则结果不会有变化。
#include <iostream>
using namespace std;
int w[105], val[105];
int dp[105][1005];
int main()
{
int t, m, res=-1;
cin >> t >> m;
for(int i=1; i<=m; i++)
cin >> w[i] >> val[i];
for(int i=1; i<=m; i++) //物品
for(int j=t; j>=0; j--) //容量,
{
if(j >= w[i])
dp[i][j] = max(dp[i-1][j-w[i]]+val[i], dp[i-1][j]);
else //只是为了好理解
dp[i][j] = dp[i-1][j];
}
cout << dp[m][t] << endl;
return 0;
}
0,1背包问题(数据大)
有n个重量和价值分别为wi, vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值的最大值
与前面唯一不同的地方便是W达到了10 ^ 9、wi到达1了10 ^ 7;
变换思路:定义dp[i+1][j]为前i个物品中挑选出价值总和为j时总重量的最小值(不存在时就是一个充分大的数INF)。
因为前0个物品什么也选择不了、故
dp[0][0] = 0;
dp[0][j]=INF;
此外 前i个物品挑选出价值总和为j时,一定有
前i-1个物品中挑选价值总和为j的部分
前i-1个物品中挑选价值总和为j-v[i]的部分,然后在选中第i个物品
于是又递推式dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]] + w[i])
最终的答案就对应于dp[n][j] <= W的最大的j;
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<map>
#define max(a,b)(a>b?a:b)
#define min(a,b)(a<b?a:b)
typedef long long ll;
using namespace std;
#define N 10010
ll dp[N],w[N]; ///此时的dp[i]表示的是;价值为i时的最小容量为dp[i];
int v[N];
int main()
{
int T,i,n,sum;
ll V;
scanf("%d%I64d",&n,&V);
sum=0;
for(i=1;i<=n;i++)
{
scanf("%I64d%d",&v[i],&w[i]);
sum=sum+v[i];
}
memset(dp,1000000010,sizeof(dp)); ///要求最小容量,初始化为最大值;
dp[0]=0;
for(i=1;i<=n;i++)
{
for(int j=sum;j>=v[i];j--)
dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
}
for(i=sum;i>=0;i--)
{
if(dp[i]<=V)
{
printf("%d\n",i); ///此处输出i,即为满足条件的最大价值
break;
}
}
return 0;
}
计算第K个答案—摩尔斯电码字典()
题目
摩尔斯电码字典
在没有电话的时代,摩尔斯电码是无线电传输领域中的一种常用代码。电码以短信号(短点,o)和长信号(长点,-)的不同组合表示各种文字。例如:o—表示英文字母J,而—表示英文字母M。
假设有一本以n个长点和m(n、m<=100)个短点组成的、包含所有信号的字典。例如:n=m=2,就会包含如下信号。
–oo
-o-o
-oo-
o–o
o-o-
oo–
这些信号已按照字典顺序排列好了。-的ASKII码是45,而o的ASCII码是111。因此,按照字典顺序,-在前,o在后。给定n和m时,编写代码计算出此字典的第k(k<=1,000,000,000,000)个信号。例如:上述字典的第四个信号是o–o。
DFS 求出生成的所有信号
// s 已经生成的信号
//n:还需要的 - 的个数
// m:还需要的 o 的个数
void generate(int n,int m,string s){
if(n == 0|| m == 0)
{
cout<<s<<endl;
return ;
}
if(n>0)generate(n-1,m,s+"-");
if(m>0)generate(n,m-1,s+"o");
}
利用动态规划求解
解题思路:首先要了解把n个长点和m个端点连到s后面,可以用二项式系数表示这种组合的个数,即 ( C n + m m ) (C^m_{n+m}) (Cn+mm)
第一个字符是长点的信号会有多少个呢?第一个是长点或,还有n-1个长点和m个短点。因此有 C n + m − 1 n − 1 个 C^{n-1}_{n+m-1}个 Cn+m−1n−1个
const int M = 1e9+100;
int bino[201][201];
//预先计算出所以必要的二项式系数
//杨辉三角 方法计算
void calcBino(){
memset(bino,0,sizeof(bino));
for(int i=0;i<=200;++i){
bino[i][0] = bino[i][i] = 1;
bino[i][j] = min(M,bino[i-1][j-1]+bino[i-1][j]);
}
}
//返回n个-和m个o组成的信号,跳过skip个信号后
//生成的信号
string kth(int n,int m,int skip){
//n == 0 ,剩余的部分只能是o
if(n == 0)return string(m,"o");
if(skip<bino[n+m-1][n-1])
return "-"+kth(n-1,m,skip);
return "o" + kth(n,m-1,skip-bino[n+m-1][n-1]);
}
龙曲线
龙曲线介绍:
龙曲线是以简单的数学规则画出一种曲线,它具有以下形态。曲线从一个简单的线段起始,按照一定规则变换此线段完成整个曲线。每形成一次变换称为“完成了一次变换代”,而每完成一代,曲线会进化到更复杂的形式。像这种“放大其一小部分的形状时,表现出与整个形状极为相似构造的图形”,就是分形。
画出龙曲线的方法暂且就称为龙曲线字符串吧!龙曲线字符串由X、Y、F、+、-组成。
那么,要画出龙曲线就从一个点起始画出如下曲线即可。
F:向前方移动一格并画线。
+:向左旋转90度。
-:向右旋转90度。
X、Y:忽略。
画出第0代龙曲线的字符串是FX。从下一代开始,按照如下方式利用前一代字符串进行字符替换,从而获得当前一代的龙曲线字符串。
X-> X+YF
Y-> FX+Y
根据上面的替换式,就有如下的1、2代龙曲线字符串。
第一代:FX+YF
第二代:FX+YF+FX-YF
我们想要求出第n代龙曲线字符串。不过,考虑到答案有可能很长,所以只想计算出第p个字符起始长度为l个字符的字符串。请编写程序实现这种功能。
输入
第一行输入测试用例的个数C(C<=50)。各测试用例的第一行分别输入3个整数,即龙曲线的世代n(0<=n<=50)、p以及l(1<=p<=1 000 000 000、1<=l<=50)。第n代龙曲线字符串的长度可假设成总是大于等于p+l的数值。
输出
每个测试用例在1行内输出第n代龙曲线字符串的第p个字符开始,输出l个字符。
示例输入
4
0 1 2
1 1 5
2 6 5
42 764853475 30
示例输出
FX
FY+YF
+FX-Y
FX-YF-FX+YF+FX-YF-FX+YF-FX-YF-
算法的详细设计思想:
分别找出字母和“+”“-”号规律,对其思想分别进行函数构造,最后进行整合,龙曲线字符串每一代都是前一代的前半部分,代数只决定了龙曲线字符串长度或者图形的终止位,但此次要求不输出图形只输出字符串规律,也就是说所有代数字符串长度内(例如第n代长度为3*2^n-1),相同位置规律是相同的。
由题目可推得:
第一代:FX+YF
第二代:FX+YF+FX-YF
第三代:FX+YF+FX-YF+FX+YF-FX-YF
由上式可看出每一代都是下一代前半部分,还可推出两个规律,字母规律和正负号规律。
字母规律:每六个一循环,第一,二,四,五位分别为F,X,Y,F.
符号规律:符号位都是3的倍数,并且规律如下。
第一代 +
第二代 + + -
第三代 + + - + + - -
1 2 3 4 5 6 7
由上式可看出,每代符号会继承上一代,并且在它们空隙处按照“+” “-”循环插入,类如,第三代继承第二代的2,4,6位置,在1,3,5,7按照“+”“-”循环插入。由此我们可以推算出在奇数位上1,3,5,7······上的奇数位是“+”,偶数位是“-”。例如7在奇数中排第
DFS 求出进化后的字符串
curve(seed,generations) = 输出初始化字符串进化generations代之后的结果。进化就是对字符中的原字符进行替换。
代码:
void curve(const string &seed,int generations){
if(generations == 0)
{
cout<<seed<<endl;
return;
}
for(int i=0;i<seed.size();++i){
if(seed[i] =='X')
curve("X+YF",generations-1);
else if(seed[i] == 'Y')
curve("FX-Y",generations-1);
else
cout<<seed[i];
}
}
动态规划求解
因为 已经定义了X和Y 替换的字符有 x L e n g t h ( n ) = x L e n g t h ( n − 1 ) + y L e n g t h ( n − 1 ) + 2 xLength(n) = xLength(n-1) +yLength(n-1)+2 xLength(n)=xLength(n−1)+yLength(n−1)+2 y L e n g t h ( n ) = x L e n g t h ( n − 1 ) + y L e n g t h ( n − 1 ) + 2 yLength(n) = xLength(n-1) +yLength(n-1)+2 yLength(n)=xLength(n−1)+yLength(n−1)+2
所以有
L
e
n
g
t
h
(
n
)
=
2
+
2
∗
L
e
n
g
t
h
(
n
−
1
)
Length(n) = 2+ 2*Length(n-1)
Length(n)=2+2∗Length(n−1)
代码:
const int MAX = 1e9+1;
// Length[i] = 把X或Y替换i次后的长度
int Length[51];
void precalc(){
Length[0] = 1;
for(int i=1;i<=50;++i)
Length[i] = min(MAX,Length[i]*2+2);
}
const string EXPAND_x = "X+YF";
const string EXPAND_Y = "FX=Y";
//返回dragonCurve 进化 generations 代后的第 skip+1 个字符
char expand(const string& dragonCurve,int generations,int skip){
if(generations == 0){
assert(skip <dragonCurve.size());
return dragonCurve[skip];
}
for(int i=0;i<dragonCurve.size();++i){
//字符串扩展时
if(dragonCurve[i] =='X' || dragonCurve[i] == 'Y'){
if(skip >= Length[generations])
skip -= Length[generations];
else if(dragonCurve[i] == 'X')
return expand(EXPAND_x,generations-1,skip);
else
return expand(EXPAND_Y,generations-1,skip);
}else if(skip>0) // +,--符号
--skip;
else
return dragonCurve[i];
}
//不扩展但需要跳过
}
旅行商问题
问题简介
一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。从图论的角度来看,该问题实质是在一个带权完全无向图中,找一个权值最小的Hamilton回路。由于该问题的可行解是所有顶点的全排列,随着顶点数的增加,会产生组合爆炸,它是一个NP完全问题。
小技巧
//输入值是布尔类型数组
当数组的长度为n时,可能的输入值个数是
2
n
2^n
2n。因此,可以把长度为n的数组解释成长度为n的二进制数。等同于A【】中的A【0】是最低位,A【1】是其上一位,A【n-1】是最高位的二进制。
记忆化搜索
为了适用记忆化的方法、应该把函数的定义修改成“尽可能最小限度地接收已选路径方面的倍息”。共有两处使用已选路径path;完成整个路径时计算整个路径长度,查看是否已经访问过某个城市。那么、把定义修改成如下形式就能少接收信息。
- 1.计算已访问路径的长度:原先shortest Path(函数返回整个路径长度,将其修改成返回剩余路径的最小长度。修改成这种形式后就不需要知道之前各城市的访问顺序,但需要知道当前位置(已选择路线中的最后一个城市)。
- 2.查看是否已经访问过某个城市:必须查看是否已经访问过某城市,但访问顺序并不重要。因此,只需传递给函数长度为n的布尔型数组即可。
通过这些修改可定义出如下函数。
shortestPath2(here,visited)=给定当前位置here,以及各个城市是否被访问过的信息,并保存在布尔型数组visited时,从here起始访问剩余城市的子路径中,返回长度最小的路径。.
代码:
int n,dist[MAX][MAX];
double cache[MAX][1<<MAX];//初始化为-1
// here :当前位置
//visited : 表示是否已访问各城市
//从here起访问剩余城市的路径中,返回长度最小的路径。
// 访问剩余所有城市的路径中,返回长度最小的路径
//假设总是从0号城市开始
double shortPath(int here,int visited){
if(visited == (1<<n)-1)return dist[here][0];
//制表
double &ret = cache[here][visited];
if(ret>=0)return ret;
ret = INF;
//尝试所有要访问的下一个城市
for(int next =0;next<n;++next){
//已经访问过的城市
if(visited &(1<<next))continue;
double cand = dist[here][next] + shortPath(next,visited+(1<<next));
ret = min(ret,cand);
}
return ret;
}
韦布巴津
题意:
鸡蛋的价格要满足一下三个条件
- 之前的鸡蛋价格是将新鸡蛋价格e中包含的数字重新排列而得到的结果
- 之前的鸡蛋价格必须小于新的鸡蛋价格
- 之前的鸡蛋价格能被m整除,
搜索所有可能
前一个位置满足以下三个条件才能使用当前位置的数字。
- 没有前一个位置,(i==0)
- 前一个位置的数字与当前数字不同
- 前一个位置的数字已被使用
代码:
//digits :对e的各位数字进行排序后的结果
string e,digits;
int n,m;
//输出所有以e的各位数字组成的结果
//price :已生成的价格
//taken :各位数字是否已被使用
void generate(string price,bool taken[15]){
if(price.size() == n){
if(price <e) // 虽然是字符串,但当位数一样时也可以利用<比较大小
cout<<price<<endl;
return ;
}
for(int i=0;i<n;++i){
if(!taken[i]&&(i == 0||digits[i-1]!=digits[i]||taken[i-1])){
taken[i] = true;
generate(price + digits[i],taken);
taken[i] = false;
}
}
}
动态规划
是否是整除,如果都能整除,只要余数相同,相减之前的数并不长重要。也就是一个小推论,如果两个数相减能整除m,那么这两个数mod m 的余数是一样的。
int MOD = 1e9+10;
//digits :对e的各位数字进行排序后的结果
string e,digits;
int n,m;
int cache[1<<14][20][2];
//从第一位开始对过去的价格进行逐个相加
//index :当前位置的小标
//taken :已使用位置的集合
//mod : 目前为止生成的价格除以m的余数
//less :目前为止生成的价格如果小于e取1,否则为0
int price(int index,int taken,int mod,int less){
if(taken == n)
return (less && mod == 0)?1:0;
// 制表
int & ret = cache[taken][mod][less];
if(ret!=-1)return ret;
ret = 0;
for(int next =0;next<n;++next){
if((taken&(1<<next)) == 0){ // 当前位置没用过
//过去的价格必须小于新的价格
if(!less && e[index]<digits[next])
continue;
//相同数字只能用一次
if(next >0&&digits[next-1] == digits[next] &&(taken &(1<<next-1)) == 0)
continue;
int nextTaken = taken|(1<<next); // 把taken 中第 next 位置为1
int nextMod = (mod *10 + digits[next]-'0')%m;
int nextLess = less|| e[index] > digits[next];
ret += price(index+1,nextTaken,nextMod,nextLess);
ret %= MOD;
}
}
return ret;
}
组合游戏
数字游戏
题目大意:
贤宇和舒夏正在用n个整数排成一行的棋盘做游戏。游戏从贤宇开始,双方轮流进行。每个人轮到自己要走棋的时候,能够做出如下两种选择。
- 可以拿走棋盘最左或最右的一个数字,被拿走的数字会从棋盘中抹掉。
- 棋盘中还剩余两个以上的数字时,可以把棋盘最右或最左的两个数字抹掉。
棋盘上的所有数字都消失后就结束游戏,每个人的分数是自己手中数字之和。两人按照“每相差1分就付给分数高的人100韩元”的方式投注,他们都全力以赴的时候,最终的分数差会是多少呢?
const int EMPTY = -987654321;
int n,board[50];
int cache[50][50];
int play(int left,int right){
if(left>right)
return 0;
int &ret = cache[left][right];
if(ret != EMPTY)
return ret;
//拿走数字时
ret = max(board[left] - play(left+1,right),board[right]- play(left,right-1));
//抹掉数字是
if(right - left +1>= 2){
ret = max(ret,-play(left+2,right));
ret = max(ret,-play(left,right-2));
}
return ret;
}
方块游戏
题目大意:
镇浩和贤焕厌倦了模拟城市和宝石迷阵游戏,打算利用家里的组合型方块进行一个新的游戏。双方轮流向5×5大小的棋盘里放置组合型方块,这些方块已经组合成L形状的三方块组和一字型的两方块组。放置时,必须与棋盘中的分割线对齐,而且不能重叠。下图是进行游戏时的棋面布局。
如图所示,各个方块组可以翻转或旋转。两名棋手轮流放置方块组,到不能再放置时,最后一个放置的棋手将会取得胜利。编写一个程序,给出一种棋面布局时,使之判断当前走棋的运行有没有取胜的方法。
实现方法:
它利用了第16章介绍的位掩码使代码变得更加简单。传递给play0的数值并不是长度为25的布尔类型数组,而是32位的整数型。要想在看(c、)格子中有无方块,只要访问第y×5+x位并查看其数值就能判断。接下来,利用位梯码就能够轻松判断出当前组合块能否放置到各个格子中。假如想把二方块组横向放置到(0,0)和(0,1)格子当中,那么第0和T位需要变成1,所以用数值表示此方块组占用的位数就是3。代码9-22也采用了这种方法,算法预先按照占用的位数计算出各个组合块能够摆出的布局,之后与当前棋面布局进行位AND运算,由此判断当前组合块能否放到此位置。precalc()会生成组合块的所有可能的组合,然后把所有可能组合占用位数的集合保存到moves[。此时,playO内部会对此结果进行循环检索,与当前的棋面布局进行位AND运算。如果得到的答案是0,就表示可以放置当前组合块.
代码:
vector<int>moves;
inline int cell(int y,int x){
return 1<<(y*5+x);
}
//预先计算出能够放置到棋盘上的各组合快的位置
void precalc(){
//计算三方块组成的L形组合快
for(int y = 0;y<4;++y)
for(int x = 0;x<4;++x){
vector<int>cells;
for(int dy = 0;dy<2;++dy)
for(int dx = 0;dx<2;++dx)
cells.push_back(cell(y+dy,x+dx));
int square = cells[0] + cells[1] + cells[2] + cells[3];
for(int i=0;i<4;++i)
moves.push_back(square-cells[i]);
}
//计算两方块组成的组合快
for(int i=0;i<5;++i)
for(int j=0;j<4;++j){
moves.push_back(cell(i,j)+cell(i,j+1));
moves.push_back(cell(j,i)+cell(j+1,i));
}
}
char cache[1<<25];
//当前棋面布局为board 时,返回骑手能否取胜
char play(int board){
char &ret = cache[board];
if(ret != -1)return ret;
ret = 0;
// 考虑所以走法
for(int i=0;i<moves.size();++i)
// 确定此走法能否适用于当前的棋面布局
if((moves[i]&board) == 0){
if(!play(board|moves[i])){
ret = 1;
break;
}
}
return ret;
}
完全背包问题
和0,1背包很像,不过选取的物品总量上限是无限大的。
C[i] 的含义是 容量为 budget 时所得的最大价值价值
都是从 低到高,不像0,1。容量是从高到低
int n,W;
int dp[N],w[110],v[110];
int main()
{
while(~scanf("%d%d",&n,&W)){
mem(dp,0);
for(int i=0;i<n;++i)
scanf("%d%d",&v[i],&w[i]);
int ans ;
for(int j=1;j<=W;++j){
ans = 0;
for(int i=0;i<n;++i)
if(j>=w[i])
dp[j] = max(dp[j] , dp[j-w[i]]+v[i]);
ans = max(ans,dp[j]);
}
printf("%d\n",ans);
}
return 0;
}
硬币问题
得到金额w 需要的最小的硬币个数
int n,m;
int dp[N],a[25];
int main()
{
while(~scanf("%d%d",&n,&m)){
for(int i=0;i<= n;++i)
dp[i] = INF;
for(int i=0;i<m;++i)
scanf("%d",&a[i]);
dp[0] = 0;
for(int i=0;i<m;++i)
for(int j=1;j<=n;++j)
if(j>=a[i])
dp[j] = min(dp[j] , dp[j-a[i]]+1);
printf("%d\n",dp[n]);
}
return 0;
}
得到金额 W 的所有方案个数
dp[i] 的含义是 得到容量 i 有 dp[i] 种方法
dp[0] = 1;
for(int i = 0;i<n;++i)
for(int j=1;j<=amount;++j)
if(j - coins[i] >= 0)
dp[j] = dp[j] + dp[j-coins[i]];