一、动态规划
动态规划的核心是 状态 和 状态转移方程。
解决动态规划的方法一般有两种
1、递推计算
递推计算的关键是边界和计算顺序
2、记忆化搜索
记忆化搜索不用事先确定计算顺序,所谓的记忆化搜索,就是给每个状态设定一个标志,当这个状态已经被计算过,通过标志判断不再重复计算。
3、DAG上的动态规划
一般的问题可以转化为有向无环图的动态规划,一般分为不知道起点和终点的最长路,已知起点和终点的最长路和最短路。
(1)在未知起点的最长路中,我们可以写出状态转移方程
d(i)=max{d[j]+1 | (i,j)∈E}
代码可以写为:
int dp(int i)
{
int& ans = d[i];
if(ans>0) return ans;
ans = 1;
for(int j=1;j<=n;j++)
if(G[i][j]) ans = max(ans,dp[j]+1);
return ans;
}
如果有多解,又要保证字典序最小,递归输出。那么:
void print_ans(int i)
{
printf("%d",i);
for (int j=1;j<=n;j++) if(G[i][j]&&d[i]==d[j]+1)
{
printf_ans(j);
break;
}
}
(2)固定终点的最长路和最短路(以硬币问题为例)
需要考虑终点不能到达的情况。
使用一个极小的数(特殊值)来表示终点不能到达代码:
int dp(int S)//S为还剩下的价值
{
int& ans =d[S]; //硬币数目
if(ans!=-1) return ans;
ans = -(1<<30);
for (int i=1;j<=n;i++) if(S>=V[i]) ans = max(ans,dp(S-V[i])+1);
return ans;
}
int dp(int S)//S为还剩下的价值
{
if(vis[S]) return d[S];
vis[S]=1;
int& ans =d[S]; //硬币数目
ans = -(1<<30);
for (int i=1;j<=n;i++) if(S>=V[i]) ans = max(ans,dp(S-V[i])+1);
return ans;
}
如果状态复杂,可以使用map来记录状态值,通过if(d.count(S))可以来判断状态是否计算过。
如果既要求最短路,又要求最长路,使用记忆化搜索需要需要写两个,这时可以采用递推的方法。
代码:
minv[0]=maxv[0]=0;
for(int i=1;i<=S;i++)
{
minv[i]=INF;
maxv[i]=-INF;
}
for(int i=1;i<=S;i++)
for(int j=1;j<=n;j++)
if(i>V[j])
{
maxv[i]= max(maxv[i],maxv[i-V[j]]+1);
minv[i]= min(minv[i],minv[i-V[j]]+1);
}
printf("%d %d\n",minv[S],maxv[S]);
//print ans
void print_ans(int *d ,int S)
{
for(int i =1;i<=n;i++)
if(S>=V[i] && d[S]==d[S-V[i]]+1)
{
printf("%d ",i);
print_ans(d,S-V[i]);
break;
}
}
实际上,无论我们使用递推计算还是记忆化搜索进行计算,计算的顺序都是从小的状态到大的状态,而大的状态可以根据小的状态的值进行求解。
传统的递推法可以表示为”对于每个状态i,计算f(i)“,这需要对于每个状态i,找到计算f(i)以来的所有状态,而另外一种方法是"对于每个状态i,更新f(i)所影响的状态",称为“刷表法”。
0-1背包问题
for(int i=n;i>=1;i++)//n种物品
for(int j=0;j<=C;j++)//j表示的为背包的剩余重量
{
d[i][j] = (i==n?0:d[i+1][j]);
if(j>=V[i]) //表示了背包剩余重量的所有可能,对这个进行了枚举
d[i][j]=max(d[i][j],d[i+1][j-V[i]]+W[i]);
//V[i]为第i个物品的体积,W[i]为第i个物品的重量
//d[i][j]表示不选物品i, d[i+1][j-V[i]]+W[i]表示现在了物品i。
}
采用滚动数组求解
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&V,&W);
for (int j = C;j>=0;j--)
if(j>=V) f[j]=max(f[j],f[j-V]+W);
}
经典动态规划模型
1、最长上升子序列问题
2、最长公共子序列问题
3、最优矩阵链乘
4、最优三角剖分
树上的动态规划
树上的动态规划,总体而言,就是计算顺序从叶子节点到根,那么需要进行DFS到叶子节点计算状态后返回。适合递归操作。
1、树的最大独立集
2、树的重心(质心)
3、树的最长路径(最远点对)
复杂状态的动态规划
1、最优配对问题
最优配对问题的状态定义为d(i,S)表示在前i个点,位于集合S的元素两两配对的最小距离和。状态转移方程为:
d(i,S)=min{ |PiPj| + d(i-1,S-{i}-{j)| j∈S}
首先通过子集二进制方式表示下表,进行枚举子集,代码如下:
//枚举i
for(int i = 0;i
2、TSP问题
TSP问题是想寻找一条道路,从起点出发,最终回到起点,最终道路的总长度最短。可以设起点和终点都为0.
可以设置状态为d(i,S)为当前在城市i, 还需访问集合S中的城市各一次后回到城市0的最短长度,则
d(i,S) = min (d(j,S-{j})+dist(i,j) | j∈S)
边界为d(i,{})= dist(0,i);
3、图的色数
图的色数问题是在一个无向图中,把图中的节点染成尽量少的颜色,使得相邻结点颜色不同。
d(S) 表示把结点S染色,所需要的颜色数的最小值,则
d(S)=d(S-S‘)+1 其中S‘是S的子集,并且内部没有边(即不存在S‘内的两个结点u和v使得u和v相邻),换句话说S‘是一个“可以染成同一种颜色”的结点集。
代码如下:
d[0]=0;
for(int S=1;S< (1<