【牛客网】埃森哲杯第十六届上海大学程序设计联赛春季赛暨上海高校金马五校赛 题解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_41156591/article/details/79977514

题目连接

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;
}



阅读更多
换一批

没有更多推荐了,返回首页