[Acwing244] 謎一樣的牛
[Acwing244] 謎一樣的牛
- 逆康託展開原理 樹狀陣列 二分查詢
有 \(n\) 頭奶牛,已知它們的身高為 \(1∼n\) 且各不相同,但不知道每頭奶牛的具體身高。
現在這 \(n\) 頭奶牛站成一列,已知第 \(i\) 頭牛前面有 \(A_i\) 頭牛比它低,求每頭奶牛的身高。
輸入格式
第 1 行:輸入整數 \(n\)。
第 \(2..n\) 行:每行輸入一個整數: \(A_i\),第 \(i\) 行表示第 \(i\) 頭牛前面有 \(A_i\) 頭牛比它低。
(注意:因為第 \(1\) 頭牛前面沒有牛,所以並沒有將它列出)輸出格式
輸出包含 \(n\) 行,每行輸出一個整數表示牛的身高。
第 \(i\)行輸出第 \(i\) 頭牛的身高。
資料範圍
\(1≤n≤10^5\)
題解
一開始聯想到了康託展開原理中逆康託展開將逆序對序列轉換為原串的方法。因為對於最後一頭牛,前面所有牛的身高狀態和其自己的身高狀態是n個1到n的不同的數,如果最後一頭牛前方比它低的牛的個數為 \(k\) 個,而這頭牛可能的高度為 \(1,2,\dots , n\),那麼,這頭牛身高至少是所有可能的高度中第\(k+1\)高的。並且它只能是第\(k+1\)高的數,下面簡單地證明它。
假設這頭牛的身高是第\(m\)高的 , \(n\geq m \geq k+2\),那麼,前方\(n\)頭牛中有\(m-1\)頭身高小於它,有\(k = m-1\)
這也是逆康託展開的基本原理。我們從最後一頭牛開始確定身高,並記錄可選的身高集合,一開始是\({1,2,\dots,n}\),如果該頭牛前方身高小於它的個數為\(k\),那麼該頭牛的身高確定為身高集合中第\(k+1\)大的數,然後將該身高從身高集合排除,計算下一頭牛。
在逆康託展開的演算法中,我們每次排除後都將可選集排序,這會造成時間複雜度過高,怎麼優化呢?
這裡使用一個元素全是1或0的陣列:
num[N] = {0,1,1,1,1,1,1,....};
我們注意到這個陣列的字首和:
Snum[1] = 1; Snum[2] = 2; Snum[3] = 3; ... Snum[n] = n;
現在我們將num[3] = 0
,那麼Snum[3~n]
都會少1。
我們可以這樣定義它:Snum[A] = B
,如果num[A]!=0
,那麼A
就是當前可選集裡第B
大的數。
那麼,維護一個數組的字首和並且支援單點修改的結構,當時是使用樹狀陣列了。
- 時間複雜度 \(O(N\log{N})\)
- 空間複雜度 \(O(N)\)
題解程式碼
// 樹狀陣列
#include<iostream>
using namespace std;
const int N = 100010;
int p[N],bit[N],h[N],n;
inline int lowbit(int x){return x&-x;}
//O(n)建全1樹狀陣列
void build(){
for(int i = 1;i<=n;i++)bit[i] = lowbit(i);
}
int query(int r){
int ans = 0;
while(r>0){
ans += bit[r];
r-= lowbit(r);
}
return ans;
}
void modify(int x, int k){
while(x<=n){
bit[x] += k;
x += lowbit(x);
}
}
int findKth(int kth){
int l=1, r =n,mid;
while(l<r){
int mid = (l+r)/2;
int rt =query(mid);
if(rt<kth){
l = mid+1;
}
else r = mid;
}
return l;
}
int main(){
scanf("%d", &n);
build();
for(int i = 2;i<=n;i++)scanf("%d", p+i);
for(int i = n;i>0;i--){
int kth = p[i];
/*/
printf("BIT: ");
for(int i = 1;i<=n;i++){
printf("%d ", query(i)-query(i-1));
}
puts("");
/*/
//找可選集中第k+1大的數
h[i] = findKth(kth+1);
//加上-1等於設定為0
modify(h[i],-1);
}
for(int i = 1;i<=n;i++){
printf("%d ",h[i]);
}
return 0;
}