秒懂错排----动态规划
什么是错排
错排就是一个数组要满足所有元素都不能在他原来的位置上,要求出这个数组所有的错排数目,如下图:
给出一个数字n,给出所有错排组合的数目,这种类似的题牛客上也有,就是信件邮箱和小朋友坐座位,都是求出所有的错排的组合,这道题有两种算法,一种的是用递归,一种的动态规划。
先说递归,还记得排列组合的算法是怎样写了的吗,忘了不用着急,下面就是排列组合的代码,而错排组合就是在排列组合的基础上加上一些限制条件就行了,请看下面代码:
这个是排列组合的代码
public int rangeNums(int n) {
int[] array = new int[n];
for(int i = 0; i < n; i++) {
array[i] = i;
}
//得到该数组的所有排列组合
return recurrence(array, 0);
}
public int recurrence(int[] nums, int index) {
if(index == nums.length) {
return 1;
}
int result = 0;
for(int i = index; i < nums.length; i++) {
swap(nums, i, index);
//把每一个从index开始排列组合的情况都加起来
result += recurrence(nums, index + 1);
swap(nums, i, index);
}
return result;
}
public void swap(int[] nums, int n, int m) {
int temp = nums[m];
nums[m] = nums[n];
nums[n] = temp;
}
只要在这个排列组合的代码上加上错排的条件限制就可以了,代码如下,观察两个代码的异同:
错排的代码
public int rangeNums(int n) {
int[] array = new int[n];
for(int i = 0; i < n; i++) {
array[i] = i;
}
//得到该数组的所有排列组合
return recurrence(array, 0);
}
public int recurrence(int[] nums, int index) {
if(index == nums.length) {
return 1;
}
int result = 0;
for(int i = index; i < nums.length; i++) {
swap(nums, i, index);
//把每一个从index开始排列组合的情况都加起来
if(i != nums[i]) {
//如果nums[i]原来不再这个座位上,就满足错排的定义
result += recurrence(nums, index + 1);
}
swap(nums, i, index);
}
return result;
}
public void swap(int[] nums, int n, int m) {
int temp = nums[m];
nums[m] = nums[n];
nums[n] = temp;
}
上面就是递归的代码,缺点可见,效率太低了,如果n = 100,那上面的代码就炸了。好的,主角登场,动态规划。直接开门见山,通常动态规划都要有一个dp[]数组,而且每个dp[i]是有意义的,在这我们这样子定义。
***dp[i] 表示的是从1…i之间所有元素的错排数目
好的,我们以1, 2, 3, 4, 5这个例子来理解为什么这道题可以用动态规划,我们定义一个dp[]数组,假设我们已经找到dp[1]…dp[4]的值,如何来求dp[5]的值
dp[1] = 0
dp[2] = 1
dp[3] = 2
dp[4] = 9
到了第5个,因为5能放的位子只有1,2,3,4一共4个位子,我们假设把5放第1个位置,然后就分两种情况,把1放到第5个位置和把1放到2,3,4个位置。
第一种情况把1放到第5 个位置,剩下的位置和元素分别为2,3,4和2,3,4,这也是错排组合的问题等于dp[3]了。
第二种情况不把1放到第5个位置,那剩下的位置和元素分别为2,3,4,5和1,2,3,4其中:
1不能放在5位置
2不能放在2位置
3不能放在3位置
4不能放在4位置
可以看出来是个元素数目为4的错排组合数目等于dp[4]
所以可以得出结论dp[n] = (n -1)(dp[n - 2] + dp[n - 1])
代码如下
public int erroNum(int n){
if(n=0)
return 0;
if(n=1)
return 0;
int []dp=new int [n+1];
dp[0]=0;
dp[1]=0;
dp[2]=1;
for(int i=3;i<=n;i++){
dp[i]=(i-1)*(dp[i-1]+dp[i-2]);
}
return dp[n];
}
细心的读者发现了,其实我们只用到了数组的前两个数据,再前面的就浪费了,没错再这里我们可以用变量代替数组就可以了,代码如下:
public int erroNum(int n){
if(n=0)
return 0;
if(n=1)
return 0;
int pre1 = 0; //用来代替dp[n - 2]
int pre2 = 1;//用来代替dp[n - 1]
int now = 0;
for(int i=3;i<=n;i++){
now = (i -1)(pre1 + pre2);
pre1 = pre2;
pre2 = now;
}
return now;
}
和递归相比,只用了O(n)的复杂度,而递归的复杂度是O(n!),其实在很多算法题中,基本都是动态规划的效率往往比递归的算法高,这也是因为动态规划是牺牲了空间换取时间的一个原因。