只是一篇读书笔记而已:
树形dp,就是在树上做动态规划。树和子树具有递归性质,可以根据父与子结点的关系设计状态转移方程,用dfs从根结点开始进行记忆化搜索来求解。
例:anniversary party
不能同时选择相邻的父、子结点使选择出的结点总权值最大,那么整个状态集合就可以分为两种情况:1.选择该结点,那么它的子结点不能被选;2.不选择该结点,那么它的子结点可选也可以不选。
即 dp[father][1]+=dp[son][0]
dp[father][0]+=max(dp[son][1],dp[son][0])
根据题目要求,用邻接表建树。然后用查询的方法查到树的根节点,放到dfs里去进行记忆化搜索:
void dfs(int father)
{
dp[father][0]=0; //初始化父节点
dp[father][1]=v[father];
for(int i=0;i<tree[father].size();i++)
{ //通过之前建立的邻接表递归父结点的每个子结点
int son=tree[father][i];
dfs(son);
dp[father][0]=dp[father][0]+max(dp[son][1],dp[son][0]);
dp[father][1]=dp[father][1]+dp[son][0];
}
}
然后合并集合的两种情况 max(dp[father][0],dp[father][1])就可以得到最终的答案。
数位dp,就是对数字的位进行的与计数有关的dp
例: 求出[0,n]的范围内不包含4的数字个数(书上提到了两种方法)
1.递推
数位dp,需要我们对数字的位数进行分析,一位一位的递推。每个状态转移中只要让该位数的首数字不为4,递推下去就能得到该位数不包含4的数字的个数。
void init()
{
dp[0][0]=1;
for(i=1;i<=18;i++) // 18位数不可能用暴力的方法
{
for(j=0;j<10;j++)
{
for(k=0;k<10;k++)
{
if(j!=4)
dp[i][j]=dp[i][j]+dp[i-1][k]; //i位数首字母不为4的状态
}
}
}
}
但是这样得到的只是某个位数不包含4的数字个数,并不是一个给定的数字中不包含4的数字个数。比如说400,它的答案是首字母为0,1,2,3四个三位数不包含4的数字个数相加而得出的,所以我们还要设计一个函数将它们加起来。
首先我们先将给出的数字的每位数存到shu数组里,然后进行计算
int sl(int len)
{
int ans=0;
for(i=len;i>=1;i--) //位数
{
for(j=0;j<shu[i];j++) //要加的数字首数字不能大于给出的数字在该位上数字的大小
{
if(j!=4)
ans=ans+dp[i][j];//将之前求出的分块整合起来得到答案的总数
}
if(shu[i]==4)
{
ans--;break;//比如说343,出现4后后面的340,341,342就没了,要break掉,顺便要减掉边界343。
}
}
return ans;
}
最后得出的答案ans还要+1,因为上述计算并不包括右边界,但是如果右边界有4也不用担心,因为之前在break前被减掉了。
2.记忆化搜索
只是把递推部分换成了dfs。记忆化搜索就是提前把dp[i]的值设为-1,后面搜索的时候如果发现dp[i]的值已经被更新过就可以直接返回,节省了继续向下搜索的步骤。
该题用记忆化搜索十分精妙,我们可以发现对于相同位数的数字中不含4的数字个数是相同的(10~19 9个 20~29 9个)因此对于相同位数只要搜索一次就够了,下次可以直接返回,大大降低了复杂度。
int dfs(int len,int ismax)
{
int ans=0,t;
if(!len) return 1; //设定边界,0位数的时候是1
if(!ismax&&dp[len]!=-1) return dp[len]; //记忆化,直接返回
t=(ismax?shu[len] : 9); //道理和递推的时候一样,比如说400,在我们搜索到三位数的时候我们的首数字只能取比4小的数字
for(int i=0;i<=t;i++) //但是如果没有搜索到当前最大位数,那么它0-9都可以取
{
if(i==4) continue;
ans=ans+dfs(len-1,ismax&&i==t);//判断是否搜索到当前的最大位数
}
if(!ismax) dp[len]=ans;//赋值,以便下一次直接返回
return ans;
}
其实状压dp也看了,但是毛都没看懂,属实不太敢乱写…
状态压缩dp,当集合中方案太多时我们把方案用二进制数来表示。二进制操作有与、或、取反、移位等,通过这些来判断状态应该如何转移。