数位dp
1.数位dp的由来:数位dp也是动态规划的一种类型,而数位dp解决的问题往往是这样的:
题目会给你一个区间,然后让你去根据这个区间去找一些符合条件的数据。
但是这样的话我们最可能想到的一点就是遍历,但是在某些情况下就会出问题。
2.例如:
①例题:要给你一个数据是0~12345数据大小,让你在其中找到相邻位数相差大小大于2的情况的数字,并去统计这些数的数量,那么应该怎么操作呢?
对于上题,如果说我们只是通过暴力遍历的方法,那么在进行操作的时候,要取到每一个数,然后对这个数字进行去位,然后判断的话,那么就会用很长的时间,在一些题目中直接就超时了,那么我们这时就应该想到通过数位dp的方法进行解决
②数位dp的思想:
要通过数位dp进行解决问题,我们首先要明白的是,其名如其操作,就是对一个数字的每个位进行操作的,所以会有一下步骤:
- 给不足最大位数的数字添加前导0(因为操作的时候,需要位数一一对应)
- 使用dfs进行递归操作(因为每次进行的操作都是一样的,递归无疑是最好的选择)
- 对正确的数字进行记录
③解决问题:
现在我们对数位dp的思想有了了解,那么我们现在对上面这道题进行解决:
int Countnumber(int n)
{
int temp = n;
int pos = 0;
vector<int> digits;
while (temp > 0) //先将每位对应的最大值加入到数组中
{
digits.emplace_back(temp % 10);
temp /= 10;
}
reverse(digits.begin(), digits.end()); //因为是尾插,所以要翻转
function<int(int, int, bool)> dfs = [&](int pos, int pre_num, bool bound)->int { //递归
if (pos >= digits.size()) //如果能递归到此处,那么证明该数是合格的,+1.
{
return 1;
}
int n = bound ? digits[pos] : 9; //查看上一位的状态,如果是最大值,那么此时这个位的最大只能取得该位
int ret = 0; //的最大值,否则可以选择0~9中任意一个数
for (int i = 0; i <= n; ++i)
{
if (abs(i - pre_num) > 2) //根据题意进行判断
{
ret += dfs(pos + 1, i, bound && i == digits[pos]);
} // 上一位如果是临界值并且当前位也是临界值时,那么下一位的操作就要被限制了
}
return ret;
};
return dfs(pos, 100, true);
}
通过这样的计算,那么题目也就解决了。
记忆化搜索
1.对于上面的数位dp其实并不是一个完美的操作,因为其做了很多重复的操作,而为了减少这些重复的操作,也就出现了一个记忆化搜索,来对重复的操作进行压缩。
2.还是同样的上面的题,对于上面题目我们发现,如果说此时遇到了11333这个数字,进行遍历完,然后继续往后遍历,就肯定会遍历到12333这个数字,此时我们其实可以当12333这个数字遍历到123前三位的时候就可以将这个数据是否为正确的数进行返回了,因为后面是不管出现什么情况,都已经在113??这五位数据中进行保存了,也就是当遍历到113的时候,以113为高位的数据有多少个正确的数都已经知道了,所以没必要进行重复的操作。
上面这只是其中的一个例子,当然还有很多,都是需要进行重复去除了,这样的操作会大大降低程序运行的效率。
3.进行修改:
int Countnumber(int n)
{
int temp = n;
int pos = 0;
vector<int> digits;
while (temp > 0) //先将每位对应的最大值加入到数组中
{
digits.emplace_back(temp % 10);
temp /= 10;
}
reverse(digits.begin(), digits.end()); //因为是尾插,所以要翻转
int memy[10][10]; //添加一个10x10的数组,去保存实现记忆化
memset(memy, -1, sizeof(memy)); //首先,先给其全部赋值为-1
function<int(int, int, bool)> dfs = [&](int pos, int pre_num, bool bound)->int { //递归
if (pos >= digits.size()) //如果能递归到此处,那么证明该数是合格的,+1.
{
return 1;
}
if (!bound && memy[pos][pre_num] != -1) //当bound为false的时候,我们发现,此时往后的遍历就和前一个数
{ //没有关系了,后面肯定是0~9,所以此时如果说这个数以及前一个数
return memy[pos][pre_num]; //在矩阵中保存的位置不是-1,那么就可以直接返回了,因为后面的
} //遍历到的大小其实就是这个数组中保存的,没必要再二次遍历了。
int n = bound ? digits[pos] : 9; //查看上一位的状态,如果是最大值,那么此时这个位的最大只能取得该位
int ret = 0; //的最大值,否则可以选择0~9中任意一个数
for (int i = 0; i <= n; ++i)
{
if (abs(i - pre_num) > 2) //根据题意进行判断
{
ret += dfs(pos + 1, i, bound && i == digits[pos]);
} // 上一位如果是临界值并且当前位也是临界值时,那么下一位的操作就要被限制了
}
if (!bound) //如果此时前一位没有限制,那么直接赋值即可。
{
memy[pos][pre_num] = ret;
}
return ret;
};
return dfs(pos, 100, true);
}
例题
解法如下:
1.根据题目可以得到,当一个数是不是好数,取决于这个数中至少要有一个数是{2 ,5 ,6 ,9 }中的一个,对于{0 ,1 ,8}可有可无,但是不可以含有{3 ,4 ,7}。
2.那么这道题就非常适合用数位dp的做法进行操作,我们之间给一个数组,将0~9对应的下标设置为-1 0 1三个数,如下:
const char check[10] = {0,0,1,-1,-1,1,1,-1,0,1};
其中表示为只要含有一个数对应的check数组中的数是-1,那么这个数就肯定不是好数,如果是对应的是1和0,那么就往后继续判断。
3.接下来就使用同样的手法进行数位dp+记忆化搜索了,解法如下:
const char check[10] = {0,0,1,-1,-1,1,1,-1,0,1};
class Solution {
public:
int rotatedDigits(int n)
{
vector<int> digits;
while(n > 0)
{
digits.emplace_back(n%10);
n/=10;
}
reverse(digits.begin(),digits.end());
int memo[5][2][2]; //标记数组,用来实现记忆化搜索
memset(memo,-1,sizeof(memo));
function<int(int,bool,bool)> dfs = [&](int pos,bool bound,bool diff)->int{ //lamdam表达式,用来实现递归
if(pos == digits.size())
{
return diff; //diff表示的是该数最后是否可以进行选择
//如果可以diff就是true也就是1
//如果不可以diff就是false,也就是0
}
if(memo[pos][bound][diff] != -1)
{
return memo[pos][bound][diff];
}
int n = bound ? digits[pos] : 9;
int ret = 0;
for(int i = 0;i <= n;++i)
{
if(check[i] != -1) //如果是-1那么就没必要往后遍历了,该数肯定不是好数
{
ret += dfs(pos+1,(bound) && (i == digits[pos]), diff || check[i]==1); //好数的基础是必须含有一个{2,5,6,9},所以是diff||check[i]
}
}
memo[pos][bound][diff] = ret;
return ret;
};
return dfs(0,true,false);
}
};