1. 程式人生 > 實用技巧 >清北學堂 2020 國慶J2考前綜合強化 Day2

清北學堂 2020 國慶J2考前綜合強化 Day2

目錄

1. 題目

T1 一

題目描述

問題描述

你是能看到第一題的 friends 呢。
——hja

眾所周知,小蔥同學擅長計算,尤其擅長計算組合數,但這個題和組合數沒什麼關係。

現在某公司有若干底層人員處理了若干訂單,每個底層人員有一箇中層管理作為他的上司,而每個中層管理也有一個高層管理作為他的上司。現在要進行年終評審,我們需要按業績對所有公司人員進行排序,每個人的業績為他所有下述的訂單數量之和,輸出這個排序結果。

輸入格式

輸入首先包含若干行字串,每行字串的格式為 姓名,訂單數

,代表一個底層人員的資訊。

接下來一行空行。

接下來包含若干行字串,每行字串的格式為 高層管理,中層管理,底層人員,代表一組從屬關係。

最後一行 eof,代表輸入結束。

輸出格式

輸出按照業績作為關鍵字進行排序,如果業績相同則按照名字字典序進行排序。如果當前輸出的是一名高層管理,則接下來先對其下屬的中層管理進行排序輸出:

  • 如果當前輸出的是一名高層管理,則接下來先對其下屬的中層管理進行排序輸出;
  • 如果當前輸出的是一名中層管理,則接下來先對其下屬的底層人員進行排序輸出。

在每個人進行輸出的時候,首先輸出其所屬層級,如果為高層管理則不用輸出任何內容,如果為中層人員需要先輸出 -,如果為底層人員需要先輸出 --

。接下來輸出該人員的姓名,再輸出 <業績>

樣例輸入

A,125
B,110
C,92
D,154

E,F,A
E,F,B
E,G,C
E,G,D
eof

樣例輸出

E<481>
-G<246>
--D<154>
--C<92>
-F<235>
--A<125>
--B<110>

資料規模與約定

對於 \(100\%\) 的資料,所有人的名字長度不超過 \(50\) 且不包含空格、逗號,所有訂單數量大小不超過 \(1000\),總共的底層人員數量不超過 \(1000\),可能出現某人的名字為 eof

Sol

噁心,讀入時直接統計 ,

的數量即可,不用判空行。

然後用 sscanf 就可以讀資訊了。

T2 二

題目描述

問題描述

你是能看到第二題的 friends 呢。
——aoao

眾所周知,小蔥同學擅長計算,尤其擅長計算組合數,但這個題和組合數沒什麼關係。

小 A 從位置 \(0\) 出發,每秒鐘速度為 \(a\)。小 B 從位置 \(x\) 出發,每秒鐘速度為 \(b(a>b)\)。小 A 每次追上小 B 之後,便掉頭開始返回位置 \(0\),抵達後再反過來追小 B,問第 \(k\) 次追上小 B 的時間為多少。

輸入格式

第一行四個整數,\(a,b,x,k\)

輸出格式

一行一個整數代表追上的時間對 \(10^9+7\) 取模後的結果,注意由於答案可能有小數存在,所以這裡小數需要在逆元意義下取模。即除以 \(k\) 等價於乘以 \(k^{10^9+5}\)

樣例輸入

2 1 1 10

樣例輸出

39365

資料規模與約定

  • 對於 \(30\%\) 的資料,答案在取模之前為整數。
  • 對於 \(80\%\) 的資料,\(k\le 10^3\)
  • 對於 \(100\%\) 的資料,\(1≤b<a≤100\)\(1≤x,k≤10^9\)

Sol

顯然第一輪就是個追及問題,時間就是 \(\dfrac{x}{a-b}\)

我們關係後幾輪,設 A 已經追上了 B,相遇點為 \(x\),現在 A 往回走,B 往前走。

則 A 再次追上 B 的時間為

\[\begin{aligned}t&=\dfrac xa+x\cdot\dfrac{a+b}a+\dfrac{1}{a-b}\\&=\dfrac xa\cdot\left(1+\dfrac{a+b}{a-b}\right)\\&=\dfrac xa\cdot\dfrac{2a}{a-b}\\&=\dfrac{2x}{a-b}\end{aligned} \]

此時,B 走到了

\[\begin{aligned}x'&=x+tb\\&=x+\dfrac{2b}{a-b}\cdot x\\&=x\cdot\left(1+\dfrac{2b}{a-b}\right)\\&=x\cdot\dfrac{a+b}{a-b}\end{aligned} \]

故每次走一次路程增加 \(\dfrac{a+b}{a-b}\) 倍,顯然,時間每次也增加 \(\dfrac{a+b}{a-b}\) 倍。

所以說總時間就是個等比數列求和,直接套公式即可。

T3 三

題目描述

問題描述

你是能看到第三題的 friends 呢。
——laekov

眾所周知,小蔥同學擅長計算,尤其擅長計算組合數,但這個題和組合數沒什麼關係。

給定字串 \(s\),求有多少種方法將 \(s\) 分割為若干偽迴文字串。所謂偽迴文字串指的是刪除至多一個字元之後能夠變成迴文串的字串。

輸入格式

一行一個字串 \(s\)

輸出格式

輸出一行一個整數代表答案對 \(10^9+7\) 取模之後的結果。

樣例輸入

aaa

樣例輸出

4

資料規模與約定

  • 對於30%30%的資料,字串長度不超過 \(10\) .
  • 對於60%60%的資料,字串長度不超過 \(100\) .
  • 對於100%100%的資料,字串長度不超過 \(2000\) .

Sol

先處理 \(l\sim r\) 是否為偽迴文串。

\(is_{l,r}\) 表示 \(l\sim r\) 是否為迴文串。

如果暴力求顯然是 \(O(n^3)\) 的。

注意到 \(is\) 有如下性質:

  • \(is_{i,i}=\bf true\)
  • \(is_{l,r}=is_{l+1,r-1}\land[a_l=a_r]\)(其中 \(a\) 為原字串),也就是中間是迴文串且兩段相同。

這樣處理就只需要 \(O(n^2)\) 的複雜度了。

那怎麼可以將其變為偽迴文串的判斷呢?

上面這個東西很像 dp,從 dp 角度考慮,至多刪一個字元,所以要多一個維度。

\(is_{l,r,s}\) 表示 \(l\sim r\) 要刪掉幾個字元可以變成迴文串(\(s\in\{0,1\}\)

轉移:

  • \(is_{l,r,0}\) 同標準迴文串:\(is_{l,r,0}=is_{l+1,r-1,0}\land[a_l=a_r]\) .
  • \(is_{l,r,1}\) 的求法是討論刪除的這個字串在哪裡:
    • 刪除 \(l\) 處的字串:\(is_{l+1,r,0}\)
    • 刪除 \(l,r\) 中間的字串:\(is_{l+1,r-1,0}\land[a_l=a_r]\)
    • 刪除 \(r\) 處的字串:\(is_{l,r-1,0}\) .

這三個取個 or 即可。

再維護一個 \(able_{l,r}=is_{l,r,0} \lor is_{l,r,1}\) 表示 \(l\sim r\) 是不是一個偽迴文串。

接下來回到原題,考慮 dp。

\(dp_i\) 表示 \(1\sim i\) 位拆分成若干個偽迴文串的方案數。

考慮如果在第 \(j\) 位可以拆分(即 \(able_{j+1,i}=\bf true\),後面為一偽迴文串),那麼 \(dp_i\) 就加上 \(dp_j\) 的方案數。

for (int i=1;i<=n;i++)
	for (int j=0;j<i;j++)
		dp[i]+=dp[j]*able[j+1][i];  // <=> if (able[j+1][i]) dp[i]+=dp[j];

T4 四

題目描述

問題描述

你是能看到第三題的 friends 呢。
——laekov

眾所周知,小蔥同學擅長計算,尤其擅長計算組合數,但這個題和組合數沒什麼關係。

現在我們有一張 \(N\times N\) 的地圖上 ABCDEF 六種方塊,其中 ABCD 為普通方塊,E 方塊可以當做 ABC 中的任意一種,而 F 方塊被消除時會把周圍八個方塊全部消除。當出現至少三個相同的方塊在一行或者一列時這些方塊就會被消除。當一系列消除完成之後,所有方塊會進行下落操作,如果產生新的可消除方塊便會繼續進行消除直到沒有能夠被消除的方塊。現在你可以執行一次交換相鄰兩個方塊的操作,問最多能夠消除多少個方塊。

輸入格式

第一行一個整數 \(N\)

接下來 \(N\) 行每行 \(N\) 個字母。

輸出格式

一行一個整數代表答案。

樣例輸入

4
AABA
BBEB
CDCD
FFFF

樣例輸出

16

資料範圍與規定

本題一共 \(10\) 組資料點,對於第 \(i\) 組資料,\(N=3i\)。注意輸入的最後一行是最下方,並且一開始可能有三個一樣的連在一起,但只有進行一次交換操作後才會開始消除。

Sol

暴力模擬。

2. 演算法 -- 資料結構

陣列:\(\rm[data,data,data,\cdots]\)

連結串列:\(\rm data\to data\to data\to \cdots\)

佇列:先進先出(First In First Out,FIFO)
棧:先進後出(First In Last Out,FILO)

堆:操作:

  • 加一個數 x
  • 刪掉最大/小的數
  • 詢問最大/小的數

存二叉樹:一維陣列,二叉樹編號,若目前節點編號為 \(i\),則其左兒子編號為 \(2i\),右兒子編號為 \(2i+1\),父親編號為 \(\left\lfloor\dfrac i2\right\rfloor\)

大根堆:任意節點都比其兒子節點大(故求最大值直接返回根)

插入操作:
若插入 \(v\),那麼先直接把 \(v\) 丟到最後一個節點的後一位。
為了滿足堆的性質,故若其比它的父節點大,那麼將其交換,若還不滿足,那麼繼續交換 \(\cdots\)

刪除操作:
顯然根是最大的,所以要把根刪掉,為了滿足它還是個二叉樹,所以將其與最後一個節點交換,然後將根刪除。
換上來那個數可能不滿足堆的性質,故若其比它的至少一個兒子大,那麼選擇最大的(為了讓其交換後不會再次違背堆的性質)那個兒子根其交換,若還不滿足,那麼繼續交換 \(\cdots\)

struct heap // 整數型別大根堆 
{
	static const N=23333;
	int n,z[N]; // z 是這棵樹
	heap(){n=0;} // 初始化 
	int top(){return z[1];} // 查詢操作 
	inline void insert(int x) // 插入操作 
	{
		z[++n]=x; int p=n;
		while ((p>1)&&(z[p]>z[p/2])) swap(z[p],z[p/2]),p/=2; // 交換並且將 p 上移 
	}
	inline void remove() // 刪除操作 
	{
		swap(z[1],z[n]); --n; int p=1;
		while (2*p<=n) // 有兒子 
		{
			int pp=2*p;
			if ((pp+1<=n)&&(z[pp+1]>z[pp])) ++pp; // 右兒子存在且比左兒子大,那麼使 pp 變成右兒子,此時 pp 是左右最大的兒子。
			if (z[p]<z[pp]){swap(z[p],z[pp]); p=pp;} // 下移 
		}
	}
};

Problem 1:給定 \(n\) 個數,求平均值最大的子區間

顯然最大值和一個更小的值取平均數會變小,所以直接輸出序列中的最大值即可。


Problem 2:給定 \(n\) 個數,求 \(\min(a_i,a_{i+1},\cdots,a_j)\cdot|i-j|\) 的最大值

如果選定區間中包含最小值,那麼 \(\min(a_i,a_{i+1},\cdots,a_j)\) 就固定了,為了最大化 \(|i-j|\),直接選整個序列即可。

如果這個區間中不包含最小值,顯然 \(l\)\(r\) 都在最小值左邊或者都在最小值右邊。

那麼兩邊的問題就和整個問題一模一樣了,遞迴求解即可。
這個最小值用 st 表維護一下即可。

st 表:

\(f_{i,j}\) 為從 \(i\) 開始 \(2^j\) 個數的最小值。
那麼顯然 \(f_{i,0}=a_i\)

\(f_{i,j}\):考慮將序列分成兩半,故轉移為 \(f_{i,j}=\min(f_{i,j-1},f_{i+2^{j-1},j-1})\) .
注意因為 \(f_{i,j}\) 是根據 \(f_{\dots\,,\,j-1}\) 轉移的,所以要先轉移 \(j\)

for (int i=1;i<=n;i++) f[i][0]=a[i]; // 初始化 
for (int j=1;(1<<j)<=n;j++)
	for (int i=1;i+(1<<j)-1<=n;i++)
		f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]); // 轉移

考慮區間最小值如何維護。

若詢問 \(\min(a_1,a_2,a_3,a_4)\),那麼顯然它就是 \(f_{1,2}\) .
若詢問 \(\min(a_1,a_2,a_3,a_4,a_5)\),注意到 \(\min\) 中多次出現數不會影響答案,故可以用兩個 \(4\) 蓋住 \(5\),答案為 \(\min(f_{1,2},f_{2,2})\) .
若詢問 \(\min(a_1,\dots,a_{15})\),同理,用兩個 8 蓋住 15 即可。

做法:

/* 預處理 */
for (int i=0;(1<<i)<=n;i++)
	for (int j=(1<<i);j<(1<<(i+1));j++) use[i]=j; // 詢問時就可以用兩個 use[i] 蓋住 i 了 
/* 詢問時 */
int l,r;
...
int len=r-l+1,j=use[len];
cout<<min(f[l][j],f[r-(1<<j)+1][j]); // 最小值 

Problem 3:給定 \(n\) 個數,\(n\) 組詢問,每次求區間中位數。

先寫個暴力,考慮優化找中位數部分。

把這個序列分成大小兩半,大的扔到大根堆,小的扔到小根堆
此時,如果 \(n\) 是偶數,那麼中位數就是兩個堆頂的平均數;否則中位數就是兩個堆頂較大的數。
考慮加入一個數,先看看它要放在左邊還是右邊,如果兩個堆的大小之差超過 \(1\) 了(不是中位數了),就把那個多的堆的堆頂丟到另一個堆裡。
複雜度 $O(n^2\log n) $

更優的做法:

把操作反過來,插入變成刪除
\(a_l \cdots a_n\) 從小到大鏈成連結串列
先維護一箇中位數指標,然後刪除就相當於把指標移動 \(0.5\) 格,這樣維護即可。
容易發現刪除和移動都是 \(O(1)\) 的,所以總複雜度為 \(O(n^2)\)


Problem 4:給定一棵 \(n\) 個點的樹,每個點的點權是 \(a_i\),有 \(m\) 組詢問,每次詢問樹中 \(p_1\)\(p_2\) 上的簡單路徑中是否能找到三個點的點權使得其能作為一個三角形的三邊。
\(1\le N,M\le 10^5,1\le a_i\le 2^{31}-1\) .

考慮一個序列使得任意三條邊都不能組成三角形。
先讓前兩條邊為 \(1\)
第三個數容易發現最小為 \(1+1=2\)
第四個數,最小為 \(1+2=3\)
第五個數,最小為 \(2+3=5\)
...

顯然這是個 Fibonacci 數列。
所以使得任意三條邊都不能組成三角形的序列最小是個 Fibonacci 數列,我們設其為 \(f\)

因為 \(1\le a_i\le 2^{31}-1\),所以算出一個常數 \(k\) 使得 \(f_k\le 2^{31}-1\le f_{k+1}\),顯然當樹上點數 \(>k\) 時一定能組成三角形。
Fibonacci 數列增長速度極快,\(k\) 差不多是 \(40\) 左右。
所以說 \(\le k\) 時直接暴力即可啦。


Problem 5(滑動視窗):給定 \(n\) 個數和一個數 \(k\),求 \(\min(a_1,a_2,\dots,a_k)\)\(\min(a_2,a_3,\dots,a_{k+1})\)\(\dots\)\(\min(a_{n-k+1},a_{n-k+2},\dots,a_n)\) .

單調佇列板子。
單調佇列就是具有單調性的佇列。

我們維護一個單調遞增的佇列。
進隊模擬:

| |      <- 3
| 3 |    <- 2 因為將 2 插入後不單調遞增了,所以刪掉 3 
| 2 |    <- 4
| 2 4 |

最小值直接就是隊頭。
考慮滑動一步時的情景。
[3 2 4] 1 => 3 [2 4 1] .

原佇列:| 2 4 | .
也就是刪一個 3,加一個 1 .
發現 3 根本不在隊頭(不影響最小值)(它也不在佇列裡啊),所以不用操作。
然後加入 1,因為 1 插入後就不單調遞增了,所以把 2 4 都彈出,佇列變成 | 1 |,目前最小值也就是 1 了。

struct monotone_queue // 單調佇列 
{
	static const int N=23333;
	int q[N],head,tail;
	monotone_queue(){head=1; tail=0;} // 初始化
	inline void pop(){++head;} // 彈出是直接彈出 
	inline void push(int x) // 插入要維護單調性 
	{
		while ((head<=tail)&&(q[tail]>x)) --tail; // 如果插進去後單調性不保證了,就不斷彈出。
		q[++tail]=x; 
	}
//	inline void getmin(){return q[head];}
};

如果要維護最大值,就改成單調遞減的單調佇列即可。


Problem 6:給定 \(n\) 個數 \(a_1,a_2,\dots,a_n\),對於任意 \(1\le l\le r\le n\),求 \(a_l+a_{l+1}+\cdots+a_r\) 的最大/小值,\(1\le N\le 10^5\)

直接暴力是 \(O(n^3)\) 的。
加上字首和優化,可以到達 \(O(n^2)\),但是還是過不了。
設這個字首和為 \(s\),那麼 \(a_l+a_{l+1}+\cdots+a_r=s_r-s_{l-1}\)
顯然要使得 \(s_{l-1}\) 儘量小。
\(s\) 陣列維護一個字首最小值 \(m_i=\min(s_1,s_2,\cdots,s_i)\)
因為使得 \(s_{l-1}\) 儘量小,故對於每個 \(i\),它的最大貢獻就是 \(s_r-m_{r-1}\),掃一遍即可。

當然也有一個比較漂亮的 貪心/dp 做法,這裡就不說了。


Problem 7:給定 \(n\) 個數 \(a_1,a_2,\dots,a_n\ge0\),和一個數 \(k\),求滿足 \(a_l+a_{l+1}+\cdots+a_r\le k\) 的最大長度。

\(s_i=a_0+a_1+a_2+\cdots+a_i\),也就是字首和。
注意到每個數都大於等於 \(0\), 所以字首和是單調遞增的。

轉換成字首和形式,就是找到一個最大的 \(r\),使得 \(s_r-s_{l-1}\le k\) .
也就是 \(s_r\le k+s_{l-1}=c\) .
列舉個 \(l\),然後二分 \(r\) 可以做到 \(O(n\log n)\) .

注意到每個數都大於等於 \(0\),所以採用如下策略:

  • 先暴力出 \(l=1\) 的時候的情況 \(r\) 最多能到多少。
  • 考慮 \(l\) 右移一位,顯然 \(r\) 可以補上幾個數,也就讓 \(r\) 一直右移,直到不滿足條件為止。

這是個 \(O(n)\) 的演算法。

Code:

int n,k,ans;
int main()
{
	scanf("%d%d",&n,&k);
	for (int i=1;i<=n;i++) scanf("%d",a+i);
	int sum=0;
	for (int l=1,r=1;l<=n;l++) // 第一次預設暴力 
	{
		while ((r<=n)&&(sum+z[r]<=k)){sum+=z[r]; ++r;} // 往右移 
		ans=max(ans,r-l); sum-=z[l];
	}
	return 0;
}

因為左右端點暴力移動次數不會超過 \(2n\),所以複雜度是 \(O(n)\) 的。


Problem 8:有 \(n\times n\) 的一個方陣,在其中還有 \(m\) 個特殊點。
兩個點的「距離」定義為它們的曼哈頓距離。
任意一個點的「權值」為其到達每個特殊點的「距離」的最小值。
\((1,1)\) 走到 \((n,n)\),求經過的權值的最小值最大是多少。要求時間複雜度低於 \(O(n^3)\)

從每個特殊點開始廣搜一遍,就能 \(O(n^2)\) 求每個點的權值了。

顯然,存在一個 \(k\),使得權值為 \(k\) 時,能從 \((1,1)\) 走到 \((n,n)\),但是權值 \(\ge k+1\) 時,就無法走到
對於只走 \(\ge m\) 的權值時,直接暴力 check,就可以做到 \(O(n^2\log n)\) 了。

同 Problem 3,將格子從大到小加進去,存在一個時刻,\((1,1)\)\((n,n)\) 就聯通了,答案就是那個格子的權值。
這個聯通性用並查集維護即可,時間複雜度 \(O(n^2)\)

並查集:

維護 \(n\) 堆元素,
操作:

  1. \(i,j\) 合併為一堆
  2. 詢問 \(i\)\(j\) 是否在同一堆

每個點都有一個箭頭,初始每個點都指向自己。

\(i,j\) 合併就把 \(i\) 一直沿箭頭走走到的點的箭頭指向 \(j\)
查詢就是查詢從 \(i\) 一直沿箭頭走走到的點與 \(j\) 一直沿箭頭走走到的點是否相同。

Code:

const int N=10005;
int f[N],n;
int get(int p) // get 為了能看懂就不壓成一行了= = 
{
	if (p==f[p]) return p;
	else return get(f[p]);
}
void merge(int p1,int p2) // 合併 
{
	p1=get(p1); p2=get(p2); // 把祖先合併 
	f[p1]=p2;
}
int main()
{
	cin>>n;
	for (int i=2;i<=n;i++) f[i]=i; // init
}

考慮並查集連成一條鏈,那麼 get 的複雜度就是 \(O(n)\) 的,有億點點慢。
考慮走的時候直接把路上的節點連到跟上,這就叫路徑壓縮。

get 函式修改如下:

int get(int p) // get 為了能看懂就不壓成一行了= = 
{
	if (p==f[p]) return p;
	else return f[p]=get(f[p]);
}