基礎算法題——小朋友排隊(樹狀數組)

小朋友排隊

問題描述
  n 個小朋友站成一排。現在要把他們按身高從低到高的順序排列,但是每次只能交換位置相鄰的兩個小朋友。

每個小朋友都有一個不高興的程度。開始的時候,所有小朋友的不高興程度都是0。

如果某個小朋友第一次被要求交換,則他的不高興程度增加1,如果第二次要求他交換,則他的不高興程度增加2(即不高興程度爲3),依次類推。當要求某個小朋友第k次交換時,他的不高興程度增加k。

請問,要讓所有小朋友按從低到高排隊,他們的不高興程度之和最小是多少。

如果有兩個小朋友身高一樣,則他們誰站在誰前面是沒有關係的。
輸入格式
  輸入的第一行包含一個整數n,表示小朋友的個數。
  第二行包含 n 個整數 H1 H2 … Hn,分別表示每個小朋友的身高。
輸出格式
  輸出一行,包含一個整數,表示小朋友的不高興程度和的最小值。
樣例輸入
3
3 2 1
樣例輸出
9
樣例說明
  首先交換身高爲3和2的小朋友,再交換身高爲3和1的小朋友,再交換身高爲2和1的小朋友,每個小朋友的不高興程度都是3,總和爲9。
數據規模和約定
  對於10%的數據, 1<=n<=10;
  對於30%的數據, 1<=n<=1000;
  對於50%的數據, 1<=n<=10000;
  對於100%的數據,1<=n<=100000,0<=Hi<=1000000。


題目分析

暴力枚舉:判斷每一個元素右邊有多少個元素小於本身,再判斷每一個元素左邊有多少個元素大於本身,得到每個元素的交換次數。用這種解法,只能得到30%的分數。
這裏就可以引出今天我們的重頭戲——樹狀數組

應用場景:快速求區間和、前綴和
①、前綴和數組(靜態數組)
新開一個數組b:每一個元素爲對象數組a初始到目前的和
eg:b[0]=a[0] b[1]=a[0]+a[1] … b[10]=a[0]+…+a[10]
這裏有個弊端:如果a數組中a[10]變化了,那b[10]到末尾的數據都要改。
時間複雜度會大大增加。

②、性能優化:樹狀數組(動態數組)樹狀數組參考資料
樹狀數組
每更新a數組維護一個C數組。
更新 updata:(向後,影響C數組後面的部分元素)
C[k]的含義是數組a上某個區間(k-lowbit(k),k]和
ps:lowbit(k)是k二進制最右邊的1代表的整數
lowbit(整數k)=k轉換爲二進制後最右邊的1代表的整數
lowbit(6)=2 ps:6的二進制爲110
C6=sum(4,6]=a[5]+a[6]
維護C數組:利用二進制每次移動就倍增的特點
假設C[index]被改動,影響了C[index+lowbit(index)]
C[index+lowbit(index)]被改動,影響C[index+lowbit(index)+lowbit(index+lowbit(index))]

當index爲奇數時,本身C[index]不會被影響
當index爲2n時,C[2k]都會被影響(k>n)

獲取 getSum:(向前,得到C數組前面部分元素和)
計算前11項和sum11=C[10]+C[8]=C[1010]+C[1000]
計算前13項和sum13=C[12]+C[8]=C[1100]+C[1000]
計算前15項和sum15=C[14]+C[12]+C[8]=C[1110]+C[1100]+C[1000]
計算第12項到第15項的和(不包含第12項)
sum=sum15-sum11
=C[14]+C[12]+C[8]-(C[10]+C[8])
=C[1110]+C[1100]+C[1000]-(C[1010]+C[1000])

樹狀數組的作用:快速求出區間和
樹狀數組本身沒有意義,只有在a數組中才用實際意義

int lowbit(int n)
{
	return n-(n&(n-1));//取n二進制下最右邊的1 
	//這裏一定要加括號,因爲&優先級小於- 
} 

void updata(int n, int i, int v, int *c)//添加 向後 
{
	for(int k=i; k<=n; k+=lowbit(k))
	{
		c[k]+=v;//影響c數組中部分元素 
	}
} 

int getSum(int *c, int i)//獲取 向前 
{
	int sum=0;
	for(int k=i; k >= 1; k-=lowbit(k))
	{
		sum+=c[k];
	}
	return sum;
} 

以上就是對樹狀數組應用的大致描述,若仍不太理解,可點擊上面的參考鏈接。


好了,接下來就是如何將樹狀數組應用到該題中呢?
通過觀察題目,我們知道每個小朋友最終不高興值與他的交換次數是有關係的
例如:t小朋友交換了4次,那他的不高興值t_ans=1+2+3+4=10
我們只要求出每個小朋友交換次數就能夠得到每個小朋友的不高興值,從而得到全部小朋友的不高興值。

思維突破:用樹狀數組c去維護身高數組a
樹狀數組:方便我們統計每個身高交換次數

第一步:
每放入a數組一個數據,更新樹狀數組c

第二步:
c數組置零
在a數組首向左看
min n含義:在已加入的元素中,下標n左側小於等於n的元素個數
sum:已加入的元素個數
cnt[n]存放sum-min n (在n的左側,n與比它大的元素交換次數)

第三步:
c數組置零
在a數組尾向右看
min n含義:在已加入的元素中,下標n右側小於等於n的元素個數
sum:已加入的元素個數
cnt[n]存放min n(在n的右側,n與比它小的元素交換次數)

第四步:
利用ans統計全部不高興值。

注意:
①、a數組實際被c數組的反映出來的。c數組是描述a數組,a數組可以不被創建,我們要統計a數組的區間和,通過c數組即可實現。
②、來回掃描。對於一個元素來說,前面比它大的一定要和它交換,而後面比它小的也一定要和它交換,這裏通過來回掃描的方式,記錄每個元素總交換次數
③、樹狀數組是沒利用c[0]的,第一個元素爲c[1] 。在本題中,0<=h[i]<=1000000,h[i]的取值範圍對應樹狀數組的下標,1<=h[i]+1(下標)<=1000001 。

代碼實現:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll lowbit(ll n)
{
	return n-(n&(n-1));
}

void updata(ll n, ll i, ll v, ll *c)
{
	for(ll k=i; k<=n; k+=lowbit(k))
	c[k]+=v; 
}

ll getSum(ll i, ll *c)
{
	ll sum=0;
	for(ll k=i; k>0; k-=lowbit(k))
		sum+=c[k];
	return sum;
}

ll h[100000];
ll c[1000000+1];
ll cnt[100000];	//存儲每個小朋友的交換次數
int main()
{
	ll n, maxH=0;
	cin>>n;
	memset(cnt, 0, sizeof(cnt));
	for(ll i=0; i<n; i++)
	{
		cin>>h[i];
		if(maxH<h[i])
			maxH=h[i];
	}
	
	//注意:樹狀數組是沒用c[0]的,第一個元素爲c[1] 
	//在本題中,0<=h[i]<=1000000
	//h[i]的取值範圍對應樹狀數組的下標
	//1<=h[i]+1(下標)<=1000001 
	//向右掃描 
	memset(c, 0, sizeof(c));
	for(ll i=0; i<n; i++)
	{
		updata(maxH+1, h[i]+1, 1, c);
		int min=getSum(h[i]+1, c);
		cnt[i]+=(i+1)-min;
	}
	//向左掃描
	memset(c, 0, sizeof(c));
	for(ll i=n-1; i>=0; i--)
	{
		updata(maxH+1, h[i]+1, 1, c);
		cnt[i]+=getSum(h[i],c);
	}
	
	long long ans=0;
	for(int i=0; i<n; i++)
	{
		ans+=(cnt[i]*(cnt[i]+1))/2;//等差求和 
	}
	cout<<ans;
	
	return 0;
}
總結

總在感嘆前輩算法設計的巧妙,樹狀數組適合用於求數組在動態變化的情況下求區間和,相對於平常求前綴和,樹狀數組使算法效率得到了極大提升。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章