1. 程式人生 > >【牛客網】埃森哲杯第十六屆上海大學程式設計聯賽春季賽暨上海高校金馬五校賽 題解

【牛客網】埃森哲杯第十六屆上海大學程式設計聯賽春季賽暨上海高校金馬五校賽 題解

題目連線

A.Wasserstein Distance

(水題)

題意:給你2行土a,b,每行都有n堆,每堆對應有a[i],b[i]克,我們可以對a中的土進行移動,移動任意堆的k克泥土到a中其他堆消耗的代價是,求實現a中每堆土有a[i]==b[i]的最小代價。

題解:由上面的堆與堆的土的移動代價公式可以知道,第一堆土直接移動k克到第三堆土消耗的代價等於它先移動k克到第二堆土,第二堆土在移動k克到第三堆土的代價,因此我們只需要從左到右對a中每堆土進行判斷,將a[i]比b[i]多了的部分直接移動到第二堆(少了同理從第二堆取),然後一路遍歷下去就行。注意long long!

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int maxn = 1e5 + 500;
ll a[maxn], b[maxn];
int main()
{
	int t;
	cin >> t;
	while (t--)
	{
		int n;
		cin >> n;
		for (int i = 0; i < n; i++)
			scanf("%lld", &a[i]);
		for (int i = 0; i < n; i++)
			scanf("%lld", &b[i]);
		ll h = 0, ans = 0;
		for (int i = 0; i < n - 1; i++)
		{
			a[i] += h;
			h = a[i] - b[i];
			ans += abs(h);
		}
		cout << ans << endl;
	}
	return 0;
}

B.合約數

(樹+dfs+素數篩)

題意:給定一棵n個節點的樹,並且根節點的編號為p,第i個節點有屬性值vali, 定義F(i): 在以i為根的子樹中,屬性值是vali的合約數的節點個數。y 是 x 的合約數是指 y 是合數且 y 是 x 的約數。小埃想知道對1e9+7取模後的結果.(注意:合數是指非質數的數,約數即是因子)

題解:首先如果正常遍歷每個結點的子樹可以得到對應的f[i]值,而對ans的貢獻值為i*f[i],可是這樣不斷重複的遍歷明顯就會TLE,而且其中很多地方都重複算了。那麼我們換個角度思考,當遍歷到一個節點時,我們知道他對是他的倍數的根結點有f[rt](這裡假定rt為他那個根)加1的貢獻,即有對ans加rt。因此我們只需要從根開始遍歷這棵樹,每遍歷到一個結點v,對它的vail值的每個合約數x的cnt[x]都加v,如果後面出現了這個因子,那麼ans加上他的cnt[vail]時就得到了之前事先預處理的v值,即子結點是根結點的合約數時的貢獻。不過在回溯時,要注意減去他對他所有合約數的預處理值(因為預處理的值是隻有當前結點的子數能使用的貢獻點)不理解的結合程式碼理解吧~還有由於題目資料vail的範圍是1~1e4,我們可以使用素數篩預處理出每個數的所有合約數。

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
#define mod (ll)(1e9+7)
const int maxn = 2e4 + 500;
vector<int>h[maxn];//合約數
bool impri[maxn];//合數
vector<int>tre[maxn];
ll cnt[maxn];
ll a[maxn];
ll ans;
void fun() {//先打表得出1e4內所有數的合約數
	for (int i = 2; i <= (int)1e4; i++) { //素數塞
		if (!impri[i]) {
			for (int j = i + i; j <= (int)1e4; j += i)
				impri[j] = true;
		}
	}
	for (int i = 4; i <= (int)1e4; i++) {//打表得到合約數
		if (!impri[i])continue;//素數
		for (int j = i; j <=(int)1e4; j+=i)
				h[j].push_back(i);//約數
	}
}
void dfs(int v, int pre)
{
	//先預處理當前結點所有合約數出現時當前節點對ans的貢獻
	for (int i = 0; i < h[a[v]].size(); i++)
		cnt[h[a[v]][i]] = (cnt[h[a[v]][i]] + v) % mod;

	//在當前結點對之前的祖先結點預處理的貢獻值取出加到ans上
	ans = (ans + cnt[a[v]]) % mod;

	for (int i = 0; i < tre[v].size(); i++)
	{
		if (tre[v][i] == pre)continue;
		dfs(tre[v][i], v);
	}

	//回溯前需要減去當前結點對後代子孫結點的貢獻,因為回溯回父結點不屬於其子孫
	for (int i = 0; i < h[a[v]].size(); i++)
		cnt[h[a[v]][i]] = (cnt[h[a[v]][i]] + mod - v) % mod;
}
int main()
{
	fun();
	int t;
	cin >> t;
	while (t--)
	{
		int n, p, u, v;
		scanf("%d%d", &n, &p);
		for (int i = 1; i <= n; i++)
			tre[i].clear();
		for (int i = 0; i < n - 1; i++)
		{
			scanf("%d%d", &u, &v);
			tre[u].push_back(v);//不能只存單向邊,因為我們不清楚哪個是父結點
			tre[v].push_back(u);//遍歷的時候跳過父結點即可
		}
		for (int i = 1; i <= n; i++)
			scanf("%lld", &a[i]);
		ans = 0;
		dfs(p, -1);
		printf("%lld\n", ans);
	}
	return 0;
}

C.序列變換

(貪心+STL)

題意:給一個t(t組樣例),每組樣例先給出一個n,隨後給出2行每行n個數據a[i],b[i].現在有三種操作,第一種:可以調換a陣列中任意2個數據;第二,三種操作總結起來就是,如果a[i]<b[i],你可以對a[i]+1,或a[i]*2,而且對a[i]進行增操作後之後對他只能進行增操作,或者如果a[i]>b[i],你可以對a[i]-1,或a[i]/2(且a[i]為奇數時只能-1),同上只能繼續減操作。要求使用最少步數使得每項a[i]==b[i]【eg:8和5需要=》3步 (8的-1操作或5的+1操作實現a==b)】

題解:對於魔法二三總結起來就是取進行操作一後的a[],b[]中每一對a[i],b[i]先比大小,得到大的一項,能除2儘量除二,不能就減1得到小的那項,至於操作一的使用這裡使用STL中的next_permutation函式實現,因為n<=9,至於對與使用STL的到的改變後的a[]要進行一個貪心的方法得到最少的操作一步數。(並且這裡使用了對a[]和b[]所有結果先進行fun1得到最小魔法二,三的使用步數的打表預處理)總的時間複雜度

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int maxn = 1e4 + 500;
int a[10], b[10], tmp[10];
int cost[10][10];
int n;
//獲取a[]陣列變換前後的最小調換次數(魔法一)
int fun2()
{
	int ret = 0;
	int t[10];
	for (int i = 0; i < n; i++)
		t[i] = tmp[i];
	for (int i = 0; i < n; i++)
	{
		if (t[i] == i)continue;
		for (int j = i + 1; j < n; j++)
		{
			if (t[j] == i)
			{
				t[j] = t[i]; ret++; break;
			}
		}
	}
	return ret;
}
//獲取當前tmp[](對a[]陣列使用魔法一後的陣列)中每個tmp[i]變為b[i]的最少次數(魔法二,三)
int fun1(int i, int j)//可先預處理出來存入cost[i][j]中
{
	int x = a[i], y = b[j];
	if (x > y)swap(x, y);
	int ret = 0;
	while (1)
	{
		if (y % 2 == 1)
		{
			ret++; y--;
		}
		if (y >> 1 >= x)
		{
			y >>= 1; ret++;
		}
		else break;
	}
	return ret + y - x;
}
int solve()
{
	int ret = fun2();
	for (int i = 0; i < n; i++)
		ret += cost[i][tmp[i]];
	return ret;
}
int main()
{
	int t;
	scanf("%d", &t);
	while (t--)
	{
		scanf("%d", &n);
		for (int i = 0; i < n; i++)
		{
			scanf("%d", &a[i]);
			tmp[i] = i;
		}
		for (int i = 0; i < n; i++)
			scanf("%d", &b[i]);
		for (int i = 0; i < n; i++)
			for (int j = 0; j < n; j++)
				cost[i][j] = fun1(i, j);
		int ans = solve();
		while (next_permutation(tmp, tmp + n))
			ans = min(ans, solve());
		printf("%d\n", ans);
	}
	return 0;
}

D.數字遊戲

待補ing~

E.小Y吃蘋果

(水題)

題意:告訴你一個人如果有X個蘋果,如果X是偶數,他就會吃掉只蘋果;如果X是奇數,他就會吃掉只蘋果。若現在只剩下1個蘋果了,問你已知他第N天前買了幾個蘋果,答案不唯一。

題解:很明顯就是2^n,n<=20,直接秒。

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
int main()
{
	int n;
	cin >> n;
	int x = 1;
	for (int i = 0; i < n; i++)
		x *= 2;
	cout << x << endl;
	return 0;
}

F.1 + 2 = 3 ?

(找規律=》二進位制斐波那契)

題意:有t組樣例,每組樣例需要你求第n個滿足這個等式的數(表示按位異或運算)

題解:找規律,會發現當最高位二進位制位在第1位,第2位,第3位,第4位時分別能有1個(0001),1個(0010),2個(0100,0101),3(1000,1001,1010)個是滿足上面等式的數,如果這裡還不能發現它們之間有聯絡可以繼續推多下一項,那麼可以發現個數上滿足斐波那契數,打表一波可以知道當二進位制最高位是第59位時,滿足上面等式的數總和已經超過了1e12也就是N的最大值,而且(2^60-1)在long long 範圍內,因此我們可以直接打一波斐波那契表,然後通過不斷的二分查詢找到二進位制1所在的位數,在轉換成10進位制即可.

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int maxn = 1e4 + 500;
ll f[maxn],a[maxn];//1000000000000
void fun()
{
	f[1] = 1; f[2] = 1;
	a[1] = 1, a[2] = 2;
	for (int i = 3;; i++)
	{
		f[i] = f[i - 1] + f[i - 2];
		a[i] = a[i - 1] + f[i];
		if (a[i] > (ll)1e12)  break;
	
	}
}
int main() 
{
	fun();
	int t;
	cin >> t;
	while (t--)
	{
		ll n;
		scanf("%lld", &n);
		int x[60]={0};
		while (n)
		{
			int p = lower_bound(a, a + 59, n) - a;
			n -= (a[p - 1] + 1);
			x[p] = 1;
		}
		ll ans = 0,k=1;
		for (int i = 1; i < 60; i++)
		{
			if (x[i] == 1)
				ans += k;
			k <<= 1;
		}
		cout << ans << endl;
	}
	return 0;
}

G.小Y做比賽

(貪心+雙指標模擬2個優先佇列)

題意:已知一個人一定可以AK一套題目,要求他的最小總罰時(從比賽開始到做出這題的時間為該題的罰時)

給出一個n,表示一共有n道題,隨後n行a[i]和b[i](表示第i道題需要a[i]的讀題時間和b[i]的敲程式碼時間,保證ac)並且他有個習慣,當剩餘題數大於2題時會先讀多一題,然後取2題敲程式碼時間最短的先敲(只有一題自然只能做它)

題解:因為前面花費的時間會對後面影響很大,所以肯定是容易的題即ac題目耗費時間少的先完成,又由於題目有而外的要求(讀2題再寫)那麼我讀的第一題必然是讀題時間最短的,然後第一道ac的題目可以是讀下一題然後ac第一道題,也可以是ac了第二題再進行其他判斷(保證每次ac題目罰時最少)

所有我們可以這麼做:

       第一步: x[]陣列儲存a的時間,xy[]陣列儲存a+b的時間,對他們都進行升序排序取(並且要記錄下標)

       第二步:取最小讀題時間的x[q=0]陣列第一個數對應的題號(預設先讀他了)然後用一個b0儲存他的b

       第三步:j接下來還需要ac多n-1個題,取題通過比較xy[p].v的值即a2+b2 和x[q].v+b0的值a2+b1

                                       (這裡1指前一題,2指後一題,若是後者小b0也需要變為b2)

       最後把b0也加上就得到最後一題的ac時間。(程式碼中t是時間軸,因此每次ac題目ans+=t】

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f3f3f
#define ll long long
const int maxn = 1e5 + 500;
struct node
{
	ll v;
	int index;
}xy[maxn],x[maxn];
ll b[maxn];
bool vis[maxn];
bool cmp(node xx, node yy)
{
	return xx.v < yy.v;
}
int main()
{
	int n;
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		scanf("%lld%lld", &x[i].v, &b[i]);
		xy[i].v = x[i].v + b[i];
		xy[i].index = i; x[i].index = i;
	}
	sort(xy, xy + n,cmp);
	sort(x, x + n,cmp);
	int p = 0, q = 0;//2個指標分別是記錄xy[],x[]遍歷到的位置
	ll t = x[q].v, b0 = b[x[q].index]; vis[x[q++].index] = true; //取第一組資料
	int cnt = 1;
	ll ans = 0;
	while (cnt++!= n)
	{
		while (vis[xy[p].index]) p++;
		while (vis[x[q].index]) q++;
		if (xy[p].v <= x[q].v + b0)
		{
			vis[xy[p].index] = true;
			t += xy[p++].v;
			ans += t;
		}
		else
		{
			vis[x[q].index] = true;
			t += x[q].v + b0;
			b0 = b[x[q++].index];
			ans += t;	
		}
	}
	t += b0;
	ans += t;
	cout << ans << endl;
	return 0;
}

H.小Y與多米諾骨牌

待補ing~

I.二數

(找規律)

題意:t組測試樣例,每組給出一個數n,數的長度小於1e5,要求輸出與它差值最小的“二數”(即每一位數都是偶數的數),當存在不同答案時取小的一項

題解:只要先找到組存在2個答案的數,在對他-1和+1會得到什麼結果可以發現規律。

【eg:與2544差值最小的二數是2600和2488(取2488),25443時取24888,25445時取26000】

通過上面的那組如果還發現不了規律就在試幾組大點的,我們這裡直接說規律,先把這串數字當作字串來看,從頭到第一個不為奇數的數都不會改變,從第一個奇數開始要考慮變大還是變小,如果變大會得到第一個奇數+1,後面所有數字變成0(9除外,可以發現他變大後差太大,只可能變小),如果變小會得到第一個奇數-1,後面數字全部變成8(如果本身就是二數那就不用改變直接輸出)

那麼接下來是判斷什麼時候需要變大什麼時候需要變小才能的到差值最小的二數呢,比賽的時候直接上了高精度減法然後比較,賽後我發現結論與4有關係,因為變大後對於相鄰2位數10-4=6,與變小的4-(10-8)=6,臨界值就是4,因此這時候你在看上面eg的25443和25445這組,對於25443當找到第一個非偶數5後我們對他後面的數進行判斷,如果為4就繼續比較下一位,一直到發現3 ,3<4因此需要變小,同理當25445找到最後那個5時發現5>4,選擇變大。不管它們後面還有多少位數結論都不變,自己理解~

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int maxn = 1e5 + 500;
char s[maxn];
int main()
{
	int t;
	scanf("%d", &t);
	while (t--)
	{
		scanf("%s", &s);
		int pos = -1, len = strlen(s);
		for (int i = 0; i < len; i++)
		{
			if ((s[i] - '0') % 2 != 0)
			{
				pos = i; break;
			}
		}
		if (pos != -1)//存在奇數,需要修改,變大或變小
		{
			if (s[pos] == '9')
				for (int i = pos; i < len; i++)
					s[i] = '8';
			else
			{
				int k = 1;
				for (int i = pos + 1; i < len; i++)
				{
					if (s[i] < '4')
					{
						k = 2; break;
					}
					else if (s[i] > '4')
					{
						k = 3; break;
					}
				}
				if (k == 1 || k == 2)
				{
					s[pos]--;
					for (int i = pos + 1; i < len; i++)
						s[i] = '8';
				}
				else
				{
					s[pos]++;
					for (int i = pos + 1; i < len; i++)
						s[i] = '0';
				}
			}
		}
		int key = 0;
		for (int i = 0; i < len; i++)
		{
			if (s[i] != '0')
			{
				key = 1; printf("%c", s[i]);
			}
			else
			{
				if(key==1)
					printf("%c", s[i]);
			}
		}
		if (key == 0)
			cout << 0;
		cout << endl;
	}
	return 0;
}

J.小Y寫文章

待補ing~

K.樹上最大值

待補ing~

L.K序列

(揹包式滾動dp+模運算)

題意:給一個數組 a,長度為 n,若某個子序列中的和為 K 的倍數,那麼這個序列被稱為“K 序列”。現在要你 對陣列 a 求出最長的子序列的長度,滿足這個序列是 K 序列。 

題解:考慮到是子序列可以不連續,所以不能直接記錄字首和維護雙指標做(子串做法)(即使這次題目資料太水,比賽中很多人當作子串暴力過了)不過我這裡還是講解正規做法。

首先可以先取走陣列中被k整除的數,其餘的數也都先%k,這樣得到的資料都是k的範圍內,對於題目資料很明顯就是在暗示揹包,然後就是剩下來的數進行模運算式的滾動揹包了。

轉換方程:dp[x ^ 1][(j + a[i]) % mod] = max(dp[x ^ 1][(j + a[i]) % mod], dp[x][j] + 1);
具體看程式碼~不好解釋哈

程式碼如下:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int maxn = 1e7 + 50;
int dp[2][maxn],a[maxn];
int main()
{
	int n, mod, xx, ans = 0,cnt=0;
	cin >> n >> mod;
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &xx);
		if (xx%mod == 0)ans++;
		else a[cnt++] = xx%mod;
	}
	int x = 0;
	for (int i = 0; i < cnt; i++)
	{
		for (int j = 0; j < mod; j++)
			if (j == 0 || dp[x][j])
				dp[x ^ 1][(j + a[i]) % mod] = max(dp[x ^ 1][(j + a[i]) % mod], dp[x][j] + 1);
		x ^= 1;
	}
	cout << ans + dp[x][0] << endl;
	return 0;
}