package leetCode.DP;
import java.util.ArrayList;
public class lc_dp_1012_numDupDigitsAtMostN {
/*
题目:
给定正整数 N,返回小于等于 N 且具有至少 1 位重复数字的正整数。
示例 1:
输入:20
输出:1
解释:具有至少 1 位重复数字的正数(<= 20)只有 11 。
示例 2:
输入:100
输出:10
解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100 。
示例 3:
输入:1000
输出:262
提示:
1 <= N <= 10^9
*/
/*
思路:困难,我思考了一天
1.f(n)=f(n-1)+boolean(n):超时,不可以
2.本题的思路是将其转换为数位DP,利用排列组合完成解题
题目需要求数字N有多少个重复的数字,可以将其转换为求数字N有多少个不重复的数字,
因为求不重复的数字可以更好地使用排列组合来求解
现在我们的重心来到要怎么将这个数字分解成可以按一定规律计算其所有不重复数位的排列组合
总体的思路是:设剩下的位置为i,剩下的数字为j,则不重复数字是在每一位依次填入与前几位不同的数字,
即选取剩下的j个数字填入剩下的i个位置,即有A(j, i)种可能,最后将其累加就是不重复数字个数
实际遍历中,我们只需要剩下的位置i这个变量,设数字N的位数为k,则剩下的数字(总可用个数-已使用) j=10-(k-i)
对于以上思路,我们还可以分为以下两种情况,第一种是高位带0,第二种是高位不带0
设N=8765
高位带0:
XXX
XX
X
高位不带0:(要确保比N小)
1XXX ~ 7XXX
80XX ~ 86XX
870X ~ 875X
8760 ~ 8765
我们知道数学中0这个数字比较特别,高位数为0即等于没有高位数,比如0096就是数字96,
这个数字尽管两个0重复了,但是这两个0属于高位,所以0096这个数字不是重复数字,即第一种情况允许高位的0可以重复
使用第一种情况求位数小于k的不重复数字的个数:因为最高位总是为0,因此一开始剩下的数字j总是为9个(1-9),
然后剩下的低位可选的数字总共有A(10-1,i)
使用第二种情况求位数为k的不重复数字的个数:一开始剩下的数字j受数字N每位上的数字影响,
设N的当前位的数字为n,则j<=n,然后剩下的低位可选的数字总共有A(10-(k-i),i)
我们具体来看一个例子,比如3562这个数字
使用第一种情况将其分解,将其排列组合可以选择的数字列出来
4th 3th 2th 1th total
0 0 0 1-9 9*A(9,0)
0 0 1-9 0-9 9*A(9,1)
0 1-9 0-9 0-9 9*A(9,2)
使用第二种情况将其分解:
4th 3th 2th 1th total
1-2 0-9 0-9 0-9 2*A(9,3)
3 0-4 0-9 0-9 5*A(8,2)
3 5 0-5 0-9 6*A(7,1)
3 5 6 0-1 2*A(6,0)
3 5 6 2 1
注:total为理想的总数,最后还需要将重复的数字剔除,比如:
1)即N中存在重复的数,可以break。如N=8837;那么到第二位时,88XX后的全部都是重复的数字,直接break
2)比较特殊的情况还有第二种情况的第一行,注意高位是从1开始,因为0的情况在第一种情况的最后一行已经考虑;
3)在每位遍历的过程中,如果碰到前面的已经重复的数字,则continue,如第二种情况第二行,
第三位为3时已经是重复的,此时contine,区别1)的情况
4)还有第二种情况的最后一行,如果前三个高位的数字不重复,并且最后要填入的2也与前面数字不重复,
则数字N本身也是一个不重复数字。这点一定要注意!
*/
public static void main(String[] args) {
lc_dp_1012_numDupDigitsAtMostN s = new lc_dp_1012_numDupDigitsAtMostN();
int res = s.numDupDigitsAtMostN(20);
System.out.println(res);
}
public int numDupDigitsAtMostN(int N) {
int n = N;
int res = 0;
ArrayList<Integer> digit = new ArrayList<>();
while (n > 0) {
digit.add(n % 10);
n /= 10;
}
int k = digit.size();
// 1.计算前k-1位不重复的数字个数
for (int i = 1; i < k; i++) {
res += 9 * permutation(9, i - 1);
}
// 2.计算位数为k时的不重复的数字个数
boolean[] isVisited = new boolean[10];
for (int i = k - 1; i >= 0; i--) {
for (int j = (i == k - 1 ? 1 : 0); j < digit.get(i); j++) {
if (isVisited[j]) {
continue;
}
res += permutation(10 - (k - i), i);
}
//注意break的点,要在循环后break,如N=8837,
// 80XX~87XX都可以找到不重复的,后面才break
if (isVisited[digit.get(i)]) {
break;
}
if (i == 0) {
res++;
}
isVisited[digit.get(i)] = true;
}
// 3.注意三个特殊情况:
// 当出现重复数字时,要continue;
// 当N中存在即为重复数字时,在计算完N前面的数字后break;
// 当数字为N时,要进行判断是否+1
return N - res;
}
// 计算阶乘n!
public int fact(int n) {
if (n == 1 || n == 0)
return 1;
return n * fact(n - 1);
}
// 计算排列A(n,m)
public int permutation(int n, int m) {
return fact(n) / fact(n - m);
}
}