題目傳送門
一、舉例說明
以 \(654923\) 為例
要判斷在 \([0, 654923]\) 區間至少有一位重複的數值的數,可以考慮其補集,即\(\color{red}所有位數均不重複的數\),用 \(N\) 減去補集即為結果。
首先可以將其分為兩種情況。
- 情況一,位數小於 \(6\) 位。所有位數均不重複的有 \(9 \times 9 \times 8 \times 7 \times 6\) 種(首位不為0)。
- 情況二,位數等於 \(6\) 位,這也可以分為兩種情況,首位小於 \(6\) 或等於 \(6\) 。對於首位小於 \(6\) 的資料,所有位數均不重複的有 \(5 \times 9 \times 8 \times 7 \times 6 \times 5\) 種情況。
對於首位等於 \(6\) ,那麼就要考慮次高位。可以定義一個 \(st\) 陣列,來記錄是否有重複的數,比如 \(654423\) ,列舉到第四位也是 \(4\) ,那麼後兩位無論是多少數字都無意義了,都一定沒有不重複的數。因此用該陣列判斷某一個數字是否已被使用,已被使用就直接跳過。
從第二位 \(j=5\) 開始列舉,於是我們第二位考慮比 \(j\) 小的數即 \(0\) 到 \(4\) ,加上後面四位不重複的數,即 \(8 \times 7 \times 6 \times 5 = P(8,4) = P(10-2,4)\),列舉後 \(st[5] = true\)。
從第三位 \(j=4\) 開始列舉,於是我們第三位考慮比 \(j\) 小的數即 \(0\) 到 \(3\) ,對於每一位數如果 \(st[k]\) 不為 \(true\) 的話,就說明此時這個數可以來當第三位數,加上後三位不重複的數即 \(7 \times 6 \times 5 = P(7,3) = P(10-3,3)\) ,列舉後 \(st[4] = true\) 。
以此將每一位數進行迴圈判定。每次迴圈完畢之後都判斷一次,對應位數 \(j\) 是否重複有 \(st[j] = true\) ,如果重複了就不用繼續往下面迴圈了,後面對應的數一定都重複。
最後跳出迴圈,同時我們在列舉的過程中,可以發現,第二位我們沒有列舉 \(5\) ,第二位沒有列舉 \(4\) …,第六位沒有列舉 \(3\) ,如果能夠列舉到這裡,說明列舉過程中沒有遇到重複的數,即這個數不是重複的數,所以要減去這個數。
二、程式碼實現
#include <bits/stdc++.h>
using namespace std;
int getSum(int a,int b){
int res = 1;
for(int i = a;b>0;b--,a--){
res *= a;
}
return res;
}
vector<int>nums;
int numDupDigitsAtMostN(int n) {
int res = n;
//先將數整理為陣列
while(n) {
nums.push_back(n % 10);
n /= 10;
}
//情況一 比給定數位數少
for(int i = nums.size() - 1;i > 0;i--){
res -= 9 * getSum(9,i-1);
}
//情況二 和給定數位數相同 但最高位小於給定數最高位
res -= (nums.back() - 1) * getSum(9,nums.size() - 1);
//情況三 和給定數位數相同 最高位相同
vector<bool> st(10);
//然後去除首位的情況,設定首位為true不可再使用
st[nums.back()] = true;
for(int i = nums.size() - 2;i >= 0;i--){
int j = nums[i];
for(int k = 0;k < j;k++){
//每次遇到前面用過的數就continue
if(st[k]) continue;
res -= getSum(10 - (nums.size() - i),i);
}
if(st[j]) {
return res;
}
st[j] = true;
}
return res-1;
}
int main()
{
int originNum;
cin>>originNum ;
cout<<numDupDigitsAtMostN(originNum);
return 0;
}
如果你看到這裡,你就浪費了2分鐘,因為這程式碼太煩了(bushi