2020牛客寒假算法基礎集訓營3 題目解析及知識點整理

這裏引用一下官方題解

A. 牛牛的DRB迷宮I

考察點:動態規劃
可以使用暴力搜索來獲取所有的方案數,但是如同斐波那契數列一樣,數據一大就不能計算,因爲我們重複算了很多次相同的內容.
改用遞推法求解,設d[i][j]爲從(1,1)走到(i,j)的方案數

d[1][1]=1
d[i][j]+= d[i-1][j]  if(s[i-1][j]=='B' || s[i-1][j]=='D')
d[i][j]+= d[i][j-1]  if(s[i-1][j]=='B' || s[i-1][j]=='R')

最後d[n][m]就是答案.

經典中的經典算法:動態規劃
kuangbin動態規劃入門專題

B. 牛牛的DRB迷宮II

考察點:構造
需要知道一個知識點:任意的非負整數都可以由若干互不相同的2的次冪相加得到,剩下的就按官方照題解的構造方式把這些2次冪得到然後根據輸入進行拼接。

C. 牛牛的數組越位

考察點:模擬
注意細節,不要只判斷(x,y)中x<0或者y<0,因爲x>=n 或者 y>=m時,也是屬於Undefined Behaviour。
同樣判斷m*x+y時,不要只判斷是否是負數,當>=n*m時同樣屬於Runtime error。

D. 牛牛與二叉樹的數組存儲

考察點:二叉樹的數組存儲方式
實際上這道題比過的比較多的2個還要簡單一些只是做的人比較少,在比賽中榜單也只是參考不能排除"歪榜"的情況。
存儲方式題目上已經介紹的比較清楚了若當前節點爲i則,左兒子爲i*2,右兒子爲i*2+1,父節點爲i/2。
因爲最後要按照節點數字順序進行輸出,所以我們在接受時可以再開一個數組來記錄每個數字的所在位置如:if (a[i] != -1) pos[a[i]] = i,最後注意判斷節點編號是否超出n是否小於1。

E. 牛牛的隨機數

考察點:期望、貢獻
因爲是異或運算我們考慮每個二進制位的貢獻,在這道題裏的期望實際上就是每個二進制位權*這個二進制位出現的概率
對於一個二進制位w來說,如果他在異或結果中存在那麼只有兩種情況:在a中存在且b中不存在、在a中不存在且在b中存在。
設A中存在二進制位w的概率爲Pa,因爲是隨機抽取所以出現概率爲區間[l1, r1]帶有這個二進制位的數字數量/區間[l1, r1]長度,同樣也可以求出Pb。
最後枚舉w爲1、2、4、8···的二進制位權,對答案的貢獻爲w*(Pa*(1-Pb)+Pb*(1-Pa))
這裏計算區間[l1, r1]帶有這個二進制位w的數字數量有個技巧。通常得到1~x的某種要求數量要比一個區間輕鬆一些,可以先計算1~r1再減去1~(l1-1)的會簡單一些。

ll calc(ll n, ll w) //1到n二進制w出現次數
{
	ll t = n / (w * 2) * w + max(0LL, n % (w * 2) - w + 1);
	return t;
}

F. 牛牛的Link Power I

考察點:前綴和思想
官方題解的前綴和講解博客
這道題的n達到了1e5,不能n2枚舉數組中數字1兩兩計算距離進行求和,現在就要想個辦法枚舉一個1計算他到前面所有1的距離和。

拿這個長度爲8的數組舉例
下標:1 2 3 4 5 6 7 8
數組:0 1 0 1 1 0 1 0

我們枚舉5號下標的1計算他前面所有1到他的距離和,如果我們暴力計算就是(5-1)+(5-4),現在優化他。
維護兩個變量sum和cnt分別表示當前枚舉的位置前面所有1的下標和與數量,當枚舉到5時sum=1+4, cnt=2此時可以直接通過5*cnt-sum得到前面所有1到他的距離和。

G. 牛牛的Link Power II

考察點:線段樹/樹狀數組、貢獻
在F的基礎上增加了兩個操作:增加1或者刪除1。
首先我們按照F題的方式來計算一遍初始答案,對於每次操作維護這個答案。如果操作的位置是p則這個位置與他前面的所有的1組成的貢獻爲p*cnt-sum,cnt爲前面1的個數sum爲前面1的下標和(同F題),與後面1的貢獻爲sum-p*cnt。如果添加1則加上共享刪除則減去。

1 1 0 1 1
a b c d e
如果把c改爲1答案增大:(c - b + c - a) + (d - c + e - c)

1 1 1 1 1
a b c d e
如果把c改爲0答案減少:(c - b + c - a) + (d - c + e - c)

實現這個功能需要動態維護cnt和sum這兩個"變量",此時需要線段樹或樹狀數組這種數據結構來實現,附一樹狀數組模板:

struct BitTree //樹狀數組維護區間和 支持單點加 區間求和
{
	ll c[N];
	void Add(int x, ll v) //在x位置加v
	{
		while (x < N)
			c[x] += v, x += lowbit(x);
	}
	ll Ask(int x)
	{
		ll t = 0;
		while (x)
			t += c[x], x -= lowbit(x);
		return t;
	}
	ll Ask(int l, int r) //查詢區間[l, r]的和
	{
		if (l > r)
			return 0;
		return Ask(r) - Ask(l - 1);
	}
}cnt, sum;

樹狀數組詳解
線段樹詳解

H. 牛牛的k合因子數

考察點:素數篩
因爲n是固定的,可以先求出1到n每個數是否爲質數,最後統計一下每個數的合數因子的個數,對於每個詢問直接輸出答案。
暴力找因子或單個根號n的算法過於緩慢,本題n爲1e5推薦並使用埃式篩算法。

const int N = 1e5 + 100;
bool isp[N]; //是否爲質數 若爲質數則存false
int cnt[N], ans[N]; //合數因子個數 答案

for (int i = 2; i <= n; ++i) //埃式篩算法
	if (!isp[i])
		for (int j = i + i; j <= n; j += i)
			isp[j] = 1;
	else //附加內容 合數直接處理
	{
		for (int j = i; j <= n; j += i)
			++cnt[j];
	}
for (int i = 2; i <= n; ++i) //統計答案
	++ans[cnt[i]];

四種素數篩法:樸素素數篩,埃氏篩,歐拉篩和區間篩

I. 牛牛的漢諾塔

查考點:記憶化搜索
這個題遞推版的動態規劃也可以解決,不過記憶化搜索更加清晰(也算是動態規劃)。下面對於這道題我們來講一下怎麼把一個會超時的暴力搜索改成記憶化搜索
我們先把題目上給的那段遞歸求解漢諾塔的代碼抄一下並稍加修改。

#include <bits/stdc++.h>
using namespace std; 
typedef long long ll;

ll f[10]; //答案 爲了不寫一堆if這裏進行編碼存在一個數組中
int code(int x, int y) //將x -> y的編碼
{
	return x * 3 + y;
}
void Hanoi(int n, int a, int b, int c)
{
	if (n == 1)
		++f[code(a, c)]; //不再輸出 改爲增加答案
	else
	{
		Hanoi(n - 1, a, c, b);
		++f[code(a, c)];
		Hanoi(n - 1, b, a, c);
	}
}
int main()
{
	int n;
	cin >> n;
	Hanoi(n, 0, 1, 2); //初始三個柱子用012代表
	ll sum = 0;
	for (int i = 0; i < 10; ++i)
		sum += f[i];
	printf("A->B:%lld\n", f[1]);
	printf("A->C:%lld\n", f[2]);
	printf("B->A:%lld\n", f[3]);
	printf("B->C:%lld\n", f[5]);
	printf("C->A:%lld\n", f[6]);
	printf("C->B:%lld\n", f[7]);
	printf("SUM:%lld\n", sum);

	return 0;
}

上面這段代碼就是題目上給出的暴力搜索代碼被我改爲了C++版本,他的複雜度非常高達到了O(2n),這個複雜度對於n=60的題目是無法接受的。
現在我們考慮一個問題,我們多次調用Hanoi這個函數並傳入相同的n、a、b、c那麼得到的結果一定相同,而且在這個遞歸調用中絕大多數都和之前傳入的參數相同,這時候我們不妨使用數組記錄一下這個結果在相同時不再遞歸調用而是直接使用記錄

#include <bits/stdc++.h>
using namespace std; 
typedef long long ll;

ll f[70][4][4][4][10]; 
//不要被這個5維數組嚇到 因爲要記錄n,a,b,c對應的大小爲10的數組 所以根據數值範圍再開4維就好了
int code(int x, int y) //將x -> y的編碼
{
	return x * 3 + y;
}
void Hanoi(int n, int a, int b, int c)
{
	if (f[n][a][b][c][0] != -1) //如果這個參數nabc被處理過則0一定不等於-1 直接返回不再計算
		return;
	memset(f[n][a][b][c], 0, sizeof(f[n][a][b][c])); //接下來我們要計算 爲-1會影響答案 清空
	if (n == 1)
		++f[n][a][b][c][code(a, c)]; //加的時候帶上參數就好了
	else
	{
		Hanoi(n - 1, a, c, b);
		for (int i = 0; i < 10; ++i) //因爲把答案都保存在最後的維度裏面了 沒有合併到一起現在來合併
			f[n][a][b][c][i] += f[n - 1][a][c][b][i]; //加上遞歸的參數
		++f[n][a][b][c][code(a, c)];
		Hanoi(n - 1, b, a, c);
		for (int i = 0; i < 10; ++i)
			f[n][a][b][c][i] += f[n - 1][b][a][c][i];
	}
}
int main()
{
	memset(f, -1, sizeof(f)); //如果這個記錄還沒有被處理則被標記爲-1
	int n;
	cin >> n;
	Hanoi(n, 0, 1, 2);
	ll sum = 0;
	for (int i = 0; i < 10; ++i)
		sum += f[n][0][1][2][i];
	printf("A->B:%lld\n", f[n][0][1][2][1]);
	printf("A->C:%lld\n", f[n][0][1][2][2]);
	printf("B->A:%lld\n", f[n][0][1][2][3]);
	printf("B->C:%lld\n", f[n][0][1][2][5]);
	printf("C->A:%lld\n", f[n][0][1][2][6]);
	printf("C->B:%lld\n", f[n][0][1][2][7]);
	printf("SUM:%lld\n", sum);

	return 0;
}

此時複雜度降低爲O(n*35)
聊聊動態規劃與記憶化搜索

J. 牛牛的寶可夢Go

考察點:動態規劃
題目要求我們按照一定的順序與條件,走過某些點並獲得他們的戰鬥力。
現在有3個怪物A,B,C,出現的時間是TA,TB,TC,假設按照 A-B-C 這個順序走,會獲得最大戰鬥力,首先最明顯的應該滿足的條件是TC>TB>TA,因爲先去拿時間大的就拿不了時間小的怪物,首先按照時間排序,現在TA<TB<TC
下面需要用上動態規劃的思想最長上升子序列相關博客
D[i]爲走到從1i所能獲得的最大戰鬥力,對於怪物B,我們枚舉從哪個怪物過來B,假如是A,他們需要滿足條件dis(a,b)<=TB-TA;他們的時間差要大於等於兩點間的距離才能從A開始並獲得B
枚舉所有滿足條件的A,來更新D[B]的答案,但是需要O(k^2)的複雜度,時限不夠,但是實際上點只有200個,枚舉的k個點,實際上都屬於這200個點,我們每次只考慮處理過的時間點,對於怪物B,枚舉N個點,每個點裏都存儲着處理過的怪物。我們需要的怪物是dis(a,b)<=TB-TA。即N個點內:TA<=TB-dis(a,b)的點的最大值,也就是區間[1,X]的最大值,這個X可以用二分法得到。
還需要維護N個點內前綴最值,再用數據結構顯得繁瑣甚至超時或者空間浪費,仔細觀察可以發現,因爲我們是按時間遍歷的,所有每次處理過的怪物的時間都是遞增的,那就好辦了直接放進去的時候,統計前綴最值就好了。
維護兩個數組time[maxn][maxn],MAX[maxn][maxn];

VAL[x] ={1,2,1,3,1}(這個是處理過的當前節點存儲的最優答案)
time[X]={1,2,3,4,5}
MAX[x] ={1,2,2,3,3}

數組開不下,但是總量只有1e5,所以用vector存儲。

發佈了277 篇原創文章 · 獲贊 52 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章