软件学院蓝桥杯选拔赛

前言

我感觉这次选拔赛选拔只是一方面,更重要的一方面是让大家当做一次练习,即使比赛发挥的不好,也不要灰心,距离蓝桥杯省赛还有3个多月时间,期间如果好好学习,多多练习,一定会有很大的提高,也一定会在比赛中取得好成绩。这次编程题,除了第一题,其他题的出题思路是,让大多数人都能得分,但得不满分。主要目的是想让大家明白,这种比赛模式下,只要按照题意写了代码,就能得到部分的分数,但想要得满分,还需要算法和数据结构来进行优化。也是想让大家认识到算法与数据结构的作用。

1. 二进制

题目:

请写出int类型数20201209的二进制数。提示:答案的长度应为32位。

答案:

00000001001101000011111011111001

题解:

主要考察进制转换,将10进制转换为2进制,每次对2取模,并将这个数除以2,直到这个数变成0为止。不过需要注意的是32位,所以求的不够32位的,需要在前面补0。java的话可以直接调用相关的方法即可转换。

代码:

#include<bits/stdc++.h>
using namespace std;
int main(){
	string ans = "";
	int n = 20211209;
	for(int i = 1; i <= 32; i++){
		ans = (char) (n%2+'0') + ans;
		n = n/2;
	} 
	cout << ans;
}

当然由于是填空题,也可以自行手算,按照10进制与2进制的关系推,也很容易得出正确答案。但还有一个最简单快速方法使用电脑自带的计算器,切换成程序员模式…

2. 日期计算

题目:

Hhy作为一名资深淀粉,他非常喜欢7这个数字,他想知道从EDG成立到夺得2021年S11全球总决赛冠军的这些天中(包括建队与夺冠这两天),年月日中一共出现了多少次7这个数字,比如2017年7月17日这一天共出现了3次7。

提示:

EDG电子竞技俱乐部(EDward Gaming),简称EDG,是一家中国电子竞技俱乐部,于2013年9月13日在广州成立。
2015年5月11日,在美国佛罗里达州塔拉哈西的MSI季中赛总决赛中,EDG以3:2战胜韩国战队SKT1,夺得英雄联盟季中赛世界冠军。
2021年11月7日,在冰岛雷克雅未克的英雄联盟全球总决赛中,EDG战队3:2战胜DK战队夺LPL第三座S赛冠军。

答案:

907

题解

题目求2013年9月13日到2021年11月7日中间出现过多少次7。可以通过编程来解决,下面的代码也是一个求日期问题的标准代码模板,不太会的同学可以参考一下。
但其实可以很容易发现,本题中7的出现次数与是不是闰年没有关系,并且出现次数的有一定的规律,比如年份中只有2017这一年包含着7,月份中只有7月包含着7,天中只要7,17,27这三天包含着7。因此即便不会用代码计算,也可以很容易的找规律的出。(如果不会编程还没找到规律,一个一个数也是一种办法…)

代码:
#include<bits/stdc++.h>
using namespace std;

int month[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};

int jud(int f){
	int ans = 0;
	while(f){
		if(f%10 == 7) ans++;
		f = f/10;
	}
	return ans;
}
int main(){
	int y1 = 2013, m1 = 9, d1 = 13;
	int y2 = 2021, m2 = 11, d2 = 7;
	int ans = jud(y1) + jud(m1) + jud(d1), f = 5;
	while(1){
		if(y1%4 == 0 && y1%100!=0 || y1%400 == 0) month[2] = 29;
		d1++;
		if(d1 > month[m1]){
			d1 = 1; m1++;
			if(m1 > 12){
				m1 = 1; y1++;
			}
		}
		ans += jud(y1) + jud(m1) + jud(d1);
		month[2] = 28;
		if(y1 == y2 && m1 == m2 && d1 == d2) break;
	}
	cout << ans;
}

3 杨辉三角

题目:

杨辉三角,是二项式系数在三角形中的一种几何排列。他有很多优美的性质。其中一条可以非常容易发现,他的每个数等于它上方两数之和。求出杨辉三角中第123456行,从左到右数第123453个数的值。

答案:

472514398

题解:

这道题有很多方法可以解决。
一、可以按照题目中给出的性质一行一行的推出最后的结果。需要注意的是如果开一个[123457][123457]的二维数组,会导致空间复杂度太大,程序运行不了。可以观察到每次计算时只需使用当前行和上一行的数组,因此可以使用滚动数组优化一下空间复杂度。最后的值很大,记得用long long 并且每次计算都对1e9+7取模。由于数据很大,程序需要跑很长时间(实测c++需要80s左右),不过是填空题影响不大。

代码:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int mod = 1e9+7;
ll arr[2][130000];
int main(){
	int n, m;
	cin >> n >> m;
	arr[0][1] = 1; 
	for(int i = 2; i <= n; i++){
		for(int j = 1; j <= i; j++){
			arr[1][j] = (arr[0][j-1] + arr[0][j] ) % mod;
		}
		for(int j = 1; j <= i; j++){
			arr[0][j] = arr[1][j];
		}
	}
	cout << arr[1][m];
}

二、可以利用杨辉三角的另一个性质解决,杨辉三角第n行m个数的值其实就是 C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1。因此求出 C 123455 123452 C_{123455}^{123452} C123455123452 的值即为答案,但直接求最后的数字很大,除法取模涉及到逆元相关的知识。可以将 C 123455 123452 C_{123455}^{123452} C123455123452 等价变成 C 123455 3 C_{123455}^{3} C1234553 通过编程或者计算器直接求出答案。

代码:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int mod = 1e9+7;

int main(){
	// 12345ll是定义为long long类型,防止计算溢出 
	cout << 123455ll*123454*123453 / 6 % mod;
}

三、还能通过其他方式求出答案,比如找规律、利用逆元等。杨辉三角有很多优美的性质,感兴趣的同学多了解一下。

4.sjm的棒棒糖

题目:

有一天sjm来到一个糖果店,糖果店的老板看sjm长的非常可爱,于是就跟sjm说,我给你两个数,一个是1136,一个1080, 你可以每次可以从中选一个大于 1 的数减 1,最后两个数都会减到 1,在减的过程中,如果两个数互质我就会给你一根棒棒糖。可是sjm对于质数很容易犯迷糊,请你帮帮sjm,找出她最多能得到多少根棒棒糖,并把最大数量输出出来。

答案:

2313

题解:

这题考察的知识点是动态规划,用使用记忆化递归也可以写,这里给出动态规划答案的代码与解析。

代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1500 + 7;
//f[i][j]代表着i 和 j 减到两个1互质的次数
int dp[maxn][maxn];
//求最大公因数
int gcd(int x,int y) {
	return y == 0 ? x : gcd(y,x % y);
}
//动态规划预处理,求出前1500的答案
void init() {
	for(int i = 1;i <= 1500;i++) {
		for(int j = 1;j <= 1500;j++) {
			//如果两个数 i 和 j 当前互质 那么他们减到1时互质的次数就等于
			// num1 = (i - 1) 和 j 减到减到1时互质的次数 + 1
			// num2 = i 和 (j - 1) 减到减到1时互质的次数 + 1
			//这两者中的较大值
			//i和j不互质则 num1 = (i - 1) 和 j 减到减到1时互质的次数 num2类似
			int num1 = dp[i - 1][j] + (gcd(i,j) == 1);
			int num2 = dp[i][j - 1] + (gcd(i,j) == 1);
			dp[i][j] = max(num1,num2);
		}
	}
}
int main() {
	init();
	int a,b;
	scanf("%d%d",&a,&b);
	printf("%d\n",dp[a][b]);
	return 0;
}

5. Beabled不相信完型填空有套路

题目

Beabled(以下简称小B)时长仰望星空的时候思考人生,静谧无人的今夜也不例外。小B回想起了高中的往事,英语是小B擅长的学科,其中一个原因是小B很喜欢高中的英语老师。有一次考前英语老师说完型填空有一个套路,每篇完型填空的答案都是A、B、C、D各五个,小B很信任老师,所以记下了这个套路。

小B在使用这个套路考试一年后,才想起要验证一下这个套路的正确性,于是他找到了至今为止考过的所有完型填空的答案,把这些答案拼成一个字符串,统计一下每个选项出现的个数,然后验证这个套路是否正确。如果每个选项出现的个数相等,则视为这个套路是正确的,输出“Beabled like English teacher the most!”;反之错误,则输出“Beabled don’t believe that cloze has a routine!”。

输入格式

输入一行字符串S代表出现过的所有选项。

输出格式

每个选项出现的数量相同,输出“Beabled最喜欢英语老师啦”。

不相同,输出“Beabled不相信完形填空有套路!”。

题解

本场签到题,使用字符串或者char数组读入后,遍历一遍统计A、B、C、D的个数,最后判断数量是否都一样即可。

代码
#include <iostream>
using namespace std;
string s;
int main()
{
	cin >> s;
	int a = 0, b = 0, c = 0, d = 0;
	for (int i = 0; i < s.size(); i++) {
		if (s[i] == 'A') a++;
		else if (s[i] == 'B') b++;
		else if (s[i] == 'C') c++;
		else d++;
	}
	if (a == b && b == c && c == d) printf("Beabled like English teacher the most!");
	else printf("Beabled don't believe that cloze has a routine!");
	return 0;
}

6.四六级考试要来了

题目

马上就要四六级考试了,小T决定在接下来 n 天内好好背背单词,于是小T列了一个详细的计划表,写下了第 i 天要背的单词个数 ai ,但是小T是个大忙人,总会有各种事情影响小T的计划,于是小T根据自己的日程安排更改计划,比如第 i 到 j 天,少背 x 个单词,第 k 到 l 天,多背 y 个单词。但是小T改计划太多次了,希望你能够根据修改记录,计算出他的单词计划。

输入格式

第一行有两个整数 n,k,代表天数与更改计划的次数。

第二行有 n 个数,第i个数则代表最初计划的第i天所背的单词数。

接下来 k 行,每行有三个数,i, j, x 代表给第i天到第j天内,单词计划改变x个。

1 <= i <= j <= n, -1e6 <= x <= 1e6。

输出格式

输出仅一行,代表修改计划后,每天所背的单词数,用空格隔开

输入样例
3 2 
1 2 3 
1 2 -1
1 3 2
输出样例
2 3 5
数据范围

数据范围:
对于百分之60的数据 0 <= n, k <= 1000

对于百分之100的数据 0 <= n, k <= 1e5

题解

本题为经典的差分算法模板题,

小T每一次修改都是对某个连续区间内的元素进行相同的更改,如果仅仅将目标修改的区间遍历一遍,逐个更改,时间复杂度将会达到O(n^2)的级别,显然是无法通过全部数据点的。所以要使用差分算法,对某个区间进行修改的操作优化为O(1)。

需要注意的是本题x的数值范围为1e6,n,k的范围是1e5,在极端的数据情况下最大数据范围为1e11,使用int会产生溢出,需要使用long long。

当然使用树状数组或者线段树实现区间修改、查询也能够通过本题,不过有点大材小用的感觉了。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
long long q[N];
int n, m, r, l, c;
int main(){
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    cin >> n >> m;
    for(int i=1;i<=n;i++){
        int t;cin >> t;
        //构建差分数组
        q[i+1] -= t; q[i] += t;
    }
    while(m--){
        cin >> l >> r >> c;
        //将l-r区间内都加上c
        q[r+1] -= c;
        q[l] += c;
    }
    for(int i=1;i<=n;i++){
        //前缀和运算
        q[i] += q[i-1];
        cout << q[i];
        if(i != n)cout << " ";
    }
    return 0;
}

7.干饭人

题目

餐厅每到中午下课,人就会超级多。同学们为了可以早点买到饭,在下课后都会马上冲到餐厅去买饭。
已知某班有 n 名同学,学号为1,2,……,n。一到下课,所有同学都同时往餐厅内的m个窗口跑去,餐厅窗口用1,2,……,m编号。第 i 号同学的跑步速度为 vi ,目标窗口数为 mi假设今天餐厅只为此班同学开放,如果有同学同时跑到一个窗口,则以学号小的学生站在前面。现在你根据已知信息,输出每个窗口所排的队列

输入格式

第一行为两个整数 n , m ,表示 n 名学生, m 个窗口
下面有 n 行,第 i 行包含3个整数 a , b ,表示第 i 个同学的跑步速度和目标要去的餐厅窗口。

输出格式

输出 n 行,第 i 行表示第 i 个窗口所排的队列,学号之间用空格隔开
(如果没有同学前往此窗口,则输出 0 )

输入样例
5 3
10 8
2 1
3 2
4 1
3 2
1 2
输出样例
3 1
2 4 5
0
题解

本题所考察的算法为排序算法结构体、c++STL的应用
首先观察本题数据范围, 0 <= n <= 2e6, 若是选择O(n^2)(如冒泡排序)或者O(nlogn)(如快速排序)的排序算法,则一定会有部分数据点出现超时的情况。
所以需要选择时间复杂度为O(n)的算法,本次通过桶排序的思想,通过空间换时间来解决此题目,需要将速度和学号按照一定的规则排序,可以发现题目中输入数据是按照学号从小到大输入的,因此学号是有序的,只需对速度进行排序,窗口数量<=10,0<速度<=1000,所以只需使用一个vector arr[15][1005]数组按照窗口和速度来存储数据,里面的的数据无需排序即为有序的;
冒泡排序代码(只能过百分之40的数据):

#include<bits/stdc++.h>
#define fi first
#define se second
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
using namespace std;
typedef long long ll;
const int maxn = 1e6+10;
vector<pair<int, int>> arr[maxn];

int main(){
	io;
	int n, m, a, b;
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a >> b;
		arr[b].push_back({a,i});
	}
	for(int i = 1; i <= m; i++){
		if(arr[i].size() == 0){
			cout << 0 << '\n';
		}else{
			for(int j = 0; j < arr[i].size(); j++){
			// 冒泡排序
			for(int k = 0; k < arr[i].size()-j-1; k++)
				if(arr[i][k].fi < arr[i][k+1].fi ){
					pair<int, int> t = arr[i][k];
					arr[i][k] = arr[i][k+1];
					arr[i][k+1] = t;
				}
			}
			for(int j = 0; j < arr[i].size(); j++){
				if(j !=0 ) cout << ' '; 
				cout << arr[i][j].se;
			}cout << '\n';
		}
	}
}

快速排序代码(只能过百分之80的数据)

#include<bits/stdc++.h>
#define fi first
#define se second
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
using namespace std;
typedef long long ll;
//const int maxn = 5e6+10;
vector<pair<int, int>> arr[15];

// sort的排序函数
bool cmp(pair<int, int> a, pair<int, int> b){
	if(a.fi == b.fi){
		return a.se < b.se;
	}
	return a.fi > b.fi;
}

int main(){
	io;
	int n, m, a, b;
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a >> b;
		arr[b].push_back({a,i});
	}
	for(int i = 1; i <= m; i++){
		if(arr[i].size() == 0){
			cout << 0 << '\n';
		}else{
			sort(arr[i].begin(), arr[i].end(), cmp);
			for(int j = 0; j < arr[i].size(); j++){
				if(j != 0) cout << ' ';
				cout << arr[i][j].se;
			}cout << '\n';
		}
	}
}

桶排序代码(正解)

#include<bits/stdc++.h>
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
using namespace std;
typedef long long ll;
vector<int> arr[15][1005];

int main(){
	io;
	int n, m, a, b;
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a >> b;
		arr[b][a].push_back(i);
	}
	for(int i = 1; i <= m; i++){
		int s = 0;
		for(int j = 1000; j >= 1; j--){
			for(int k = 0; k < arr[i][j].size(); k++){
				if(s == 1) cout << ' ';
				cout << arr[i][j][k];
				s = 1;
			}
		}
		if(s == 0) cout << 0; 
		cout << '\n';
	}
}

8.跟djh一起数数的日子

djh有一天发现了一道题,想了很久却一直做不对。题意为:给定两个整数 a 和 b,求 a 和 b之间的所有数字中 0∼9 的出现次数。

例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 10 次,1 出现 10 次,2 出现 7次,3 出现 3次等等…

输入格式

输入包含多组测试数据。

每组测试数据占一行,包含两个整数 a 和 b。

当读入一行为 0 时,表示输入终止,且该行不作处理。

输出格式

每组数据输出一个结果,每个结果占一行。

每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。

数据范围

对于百分之10的数据 -10^3 < a , b < 10^3

对于百分之50的数据 -10^6 < a , b < 10^6

对于百分之70的数据 -10^9 < a , b < 10^9

对于百分之100的数据 -10^14 < a , b < 10^14

题解

本次暴力枚举O(n)的时间复杂度可以过百分之50的数据,正解是通过寻找数字中单个数字出现次数的规律来得出正确答案。可以通过规律求得1-n中单个数字k出现的次数(具体看代码注释),此外数据有负数,需要分类讨论一下。

代码

暴力代码

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

ll arr[15];
void cnt(ll n){
	if(n == 0) arr[0]++;
	while(n){
		arr[n%10]++;
		n = n/10;
	}
}
int main(){
	ll a, b;
	cin >> a >> b;
	ll ans = 0;
	for(int i = a; i <= b; i++){
		cnt(abs(i));
	}
	for(int i = 0; i <= 9; i++){
		cout << arr[i];
		if(i != 9) cout << ' ';
	}
}

正解

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long LL;
LL a, b;

//统计这个数字有几位 
int dgt(LL x)
{
	int res = 1;
	while(x)
	{
		x /= 10;
		res++;
	}
	return res;
}

//统计1到n之间 i这个数字一共出现几次 
LL cnt(LL n, int i)
{
	//d为n的位数 
	int d = dgt(n);
	//res为i一共出现几次 
	LL res = 0;
	//从右边第一位也就是个位开始找,看这位置上i一共出现了几次 
	for(int j = 1; j <= d; j++)
	{
		//如果j是个位,p就是1,是十位,p就是10
		//l为j位左边部分的数值 也就是下面的abc
		//r为j位右边部分的数值
		//dj为改数字在j位上的值 
        //比如1234567
        //如果我看的是j = 4位也就是千位上,那么p= 1000,l=123,r=567,dj=4
		LL p = pow(10, j - 1), l = n / p / 10,  r = n % p, dj = (n / p) % 10;
		//如果不是找0,那么计算第j位左边的整数小于l (xxx = 000 ~ abc - 1)的情况
		if(i) res += l * p;
		//如果是0,那么计算第j位左边的整数从1开始小于l (xxx = 001 ~ abc - 1)的情况
		if( !i && l) res += (l - 1) * p;
		
		//如果第j位比他大,那么加上  (xx = 000 ~ 999 )的情况
		if(dj > i  && (i || l)) res += p;
		// 计算第j位左边的整数等于l (xxx = abc)的情况
		if(dj == i && (i || l)) res += (r + 1); 
	}
	return res;
}

int main()
{
	cin >> a > b;
	//判断是否都小于0,如果是则把他们转化为两个正数 
	if( a <= 0 && b <= 0)
	{
		a = -a;
		b = -b;
		swap(a, b);
	} 
	//找这个区间含几个i 
	for(int i = 0; i <= 9; i++)
	{
		//如果两个数都大于0 
		if( a >= 0 )
		{
			LL ans = cnt(b, i) - cnt(a - 1, i);
			if(i == 0 && a == 0) ans ++;
			cout << ans << " ";
		}
		//或者有一个小于0 
		else 
		{
			LL ans =  cnt(b, i)  + cnt(-a, i);
			if(i == 0) ans ++;
			cout << ans <<" ";
		}
	}
	return 0;
}

另一种正解

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

// 求0-n中数字k出现的次数 
ll cnt(ll n, int k){
	ll f = n;
    ll c = 1, res = 0, rem = 0;
    while(n){
        if(n % 10 == k) res += n / 10 * c + rem + 1;
        else res += (n / 10 + (n % 10 > k-1)) * c;  
        rem += n % 10 * c;
        if(k == 0) res -= c;
		c *= 10;
        n /= 10;
    }
    if(k == 0) res += 1;
    return res;
}

int main(){
	ll a, b;
	cin >> a >> b;
	for(int i = 0; i <= 9; i++){
		ll ans = 0;
		if(a >= 0 && b >= 0 || a <= 0 && b <= 0){
			if(a >= 0){
				ans += cnt( abs(b), i );
				if(a != 0) ans -= cnt( abs(a-1), i );
			}else{
				ans += cnt(abs(a), i );
				if(b != 0) ans -= cnt(abs(b+1), i );
			}
		}else{
			ans += cnt(0-a, i);
			ans += cnt(b, i);
			if(i == 0) ans--;
		}
		cout << ans;
		if(i!=9) cout << ' ';
	}
}

9. Beabled的奇妙梦中旅程

Beabled(简称小B)喜欢睡觉,有时候一睡就是一整天,因此小B因睡觉耽误了好多事而受责怪。有一次,小B下午有一个重要的会议,但是小B现在很困,所以小B决定午睡一会,但是很不巧,做梦的时候小B发现自己被困在梦里出不去了,小B非常慌忙,但是却无能为力。这时有一个士兵过来告诉小B,说他有办法让小B逃脱梦境,士兵说:

附近有n个城堡,小B所处的位置就是1号古堡,每个古堡之间原本有互通的道路,但是因为政权问题,部分道路被阻断,现只存在部分连通的道路,而且每条连通的道路长度也不尽相同。在第n个城堡里,有女巫可以制造摆脱梦境的解药,你去要找到这个女巫,才有机会摆脱梦境。

小B听完后很感动,正准备动身前往时,士兵把小B拦下,说:

别着急,我还没告诉你古堡的分布呢,你现在去怎么能找到?我将会告诉你有m条连通两个古堡的道路,以及每条道路的路径长度l,剩下的就交给你了。

小B深知是场梦,听完还是很感动!在筹备好行动路线后就以最短的路径前往第n个古堡了。

最终小B成功逃脱了梦境,赶上了重要的会议。

那么聪明的你一定也能算出小B前往第n个古堡最短的路径是多少吧?

输入格式

第一行包含整数n和m。

接下来m行每行包含三个整数a,b,c,表示存在一条从点a到b的有向边,边长为c。

输出格式

输出一个整数,表示小B从1号古堡到n号古堡的最短距离。

若路径不存在,则输出“小B is Game Over!”。

数据范围

对于百分之20的数据n,m<=50,

对于百分之40的数据,n<=500,m<=1000,

对于百分之70的数据,n<=5000,m<=10000,

对于百分之100的数据,n,m<=150000。

题解

本题主要考察最短路径算法,是一个标准的模板题,但也可以通过奇形怪状的暴力骗取部分的分数。
使用floyd算法可以通过百分之40的数据,使用朴素dijkstra可以通过百分之70的样例。正解为堆优化版的dijkstra。感兴趣的可以在网上搜索一下相关知识,有很多相关的博客。
这里给出堆优化版的dijkstra解析。

堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离最短的点O(n^2)可以使用最小堆优化。

  1. 一号点的距离初始化为零,其他点初始化成无穷大。
  2. 将一号点放入堆中。
  3. 不断循环,直到堆空。每一次循环中执行的操作为:
    弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
    用该点更新临界点的距离,若更新成功就加入到堆中。
    时间复杂度分析
    寻找路径最短的点:O(n)

加入集合S:O(n)

更新距离:O(mlogn)

eac97a72da924b845002f98e1c727f3.png
55fec2d8dd5e91cdc6f8b5d179a7d19.png

代码

#include<iostream>
#include<cstring>
#include<queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 150010; 

// 稀疏图用邻接表来存
int h[N], e[N], ne[N], idx;
int w[N]; // 用来存权重
int dist[N];
bool st[N]; // 如果为true说明这个点的最短路径已经确定
int n, m;
void add(int x, int y, int c)
{
    w[idx] = c; // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中
    e[idx] = y; // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),并
    ne[idx] = h[x]; // 标记st为true,所以下一次弹出3+x会continue不会向下执行。
    h[x] = idx++;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
    // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时    
    // 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
    heap.push({ 0, 1 }); // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
    while(heap.size())
    {
        PII k = heap.top(); // 取不在集合S中距离最短的点
        heap.pop();
        int ver = k.second, distance = k.first;
        if(st[ver]) continue;
        st[ver] = true;

        for(int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
            if(dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({ dist[j], j });
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d%d", &n, &m);
    while (m--)
    {
        int x, y, c;
        scanf("%d%d%d", &x, &y, &c);
        add(x, y, c);
    }
    cout << dijkstra() << endl;
    return 0;
}

java版

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.PriorityQueue;

public class Main{
    static int N = 150010;
    static int n;
    static int[] h = new int[N];
    static int[] e = new int[N];
    static int[] ne = new int[N];
    static int[] w = new int[N];
    static int idx = 0;
    static int[] dist = new int[N];// 存储1号点到每个点的最短距离
    static boolean[] st = new boolean[N];
    static int INF = 0x3f3f3f3f;//设置无穷大
    public static void add(int a,int b,int c)
    {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx ++;
    }
    // 求1号点到n号点的最短路,如果不存在则返回-1
    public static int dijkstra()
    {
        //维护当前未在st中标记过且离源点最近的点
        PriorityQueue<PIIs> queue = new PriorityQueue<PIIs>();
        Arrays.fill(dist, INF);
        dist[1] = 0;
        queue.add(new PIIs(0,1));
        while(!queue.isEmpty())
        {
            //1、找到当前未在s中出现过且离源点最近的点
            PIIs p = queue.poll();
            int t = p.getSecond();
            int distance = p.getFirst();
            if(st[t]) continue;
            //2、将该点进行标记
            st[t] = true;
            //3、用t更新其他点的距离
            for(int i = h[t];i != -1;i = ne[i])
            {
                int j = e[i];
                if(dist[j] > distance + w[i])
                {
                    dist[j] = distance + w[i];
                    queue.add(new PIIs(dist[j],j));
                }
            }

        }
        if(dist[n] == INF) return -1;
        return dist[n];
    }

    public static void main(String[] args) throws IOException{
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String[] str1 = reader.readLine().split(" ");
        n = Integer.parseInt(str1[0]);
        int m = Integer.parseInt(str1[1]);
        Arrays.fill(h, -1);
        while(m -- > 0)
        {
            String[] str2 = reader.readLine().split(" ");
            int a = Integer.parseInt(str2[0]);
            int b = Integer.parseInt(str2[1]);
            int c = Integer.parseInt(str2[2]);
            add(a,b,c);
        }
        System.out.println(dijkstra());
    }
}

class PIIs implements Comparable<PIIs>{
    private int first;//距离值
    private int second;//点编号

    public int getFirst()
    {
        return this.first;
    }
    public int getSecond()
    {
        return this.second;
    }
    public PIIs(int first,int second)
    {
        this.first = first;
        this.second = second;
    }
    @Override
    public int compareTo(PIIs o) {
        // TODO 自动生成的方法存根
        return Integer.compare(first, o.first);
    }
}

10.夺宝游戏

题目描述

小H在玩一个游戏,游戏的规则是这样的:有一块3*n个格子组成的区域,即有3行n列,每一块格子上都有一个价值为v的宝物,游戏开始前小H在这块区域的起点:左上角(1,1)处,他需要要走到这块区域的终点:右下角(3,n)处。

他每次只能向右或者向下移动一格,不能向左或者向上移动。每走过一块格子就会将这块格子的宝物收入囊中(小H出生在(1,1)处,所以(1,1)处的宝物可以直接获得),到达终点后,小H拥有的宝物价值必须大于等于K,才能赢得游戏。请问小H有多少种走法能最终赢得游戏。

输入描述:

第一行两个正整数n, k。0 <= k <= 1e10。

接下来三行,每行n个整数,第i行第j个整数vij表示该块格子宝物的价值。0<=v<=1e4。

输出描述:

小H赢得游戏的不同方法数。

输入样例:
3 10
1 1 1
2 2 2
3 3 3
输出样例:
4
数据范围:

对于百分之10的数据n<=20,

对于百分之40的数据n<=5000,

对于百分之70的数据n<=50000,

对于百分之100的数据n<=500000。

题解

本题是这场比赛中相对比较难的一道题,但部分数据很水,依旧可以使用暴力骗取一些分数。
由于图只有3行,且不能往上走,因此小H只有两次向下走的机会。通过预处理前缀和做优化,然后枚举两次往下走的下标, 通过sum1[i]+sum2[j]−sum2[i−1]+sum3[n]−sum3[j−1]≥k 计算符合条件的数目,时间复杂度为O(n^2),可以通过百分之40的数据。
正解需要用到前缀和、树状数组(线段树)、离散化来解决。
首先预处理出每行的前缀和数组,接着注意到行数只有三,于是我们用 i 表示从第一行下到第二行的下标,用 j表示从第二行下到第三行的下标,那么问题可以转化成,求满足 sum1[i]+sum2[j]−sum2[i−1]+sum3[n]−sum3[j−1]≥k的 (i,j)对数。

我们对上式移项,变为 sum2[j]−sum3[j−1] ≥ k+sum2[i−1]−sum1[i]−sum3[n] (i<=j)。显然可以对其离散化后利用树状数组或者线段树进行求解。

​ 时间复杂度: O(n×log(n))。

前缀和代码(通过百分之40数据)

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int maxn = 5e5+10;
ll arr[4][maxn];
ll sum[4][maxn];
int main(){
	ll n, k;
	cin >> n >> k;
	for(int i = 1; i <= 3; i++){
		for(int j = 1; j <= n; j++){
			cin >> arr[i][j];
			sum[i][j] = sum[i][j-1] + arr[i][j];
		}
	}
	ll ans = 0;
	for(int i = 1; i <= n; i++){
		for(int j = i; j <= n; j++){
			if(sum[1][i] + sum[2][j]- sum[2][i-1] + sum[3][n] - sum[3][j-1] >= k) ans++;
		}
	}
	cout << ans;
}

正解代码

#include<bits/stdc++.h>
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
using namespace std;

typedef long long ll;
const int maxn = 1e6+10;
const int mod = 1e9+7;

ll bit[maxn*4], n, k;

void add(int p, ll x){ 
    while(p <= n) bit[p] += x, p += p & -p;
}

ll ask(ll p){ 
    ll res = 0;
    while(p) res += bit[p], p -= p & -p;
    return res;
}
ll range_ask(ll l, ll r){ 
    return ask(r) - ask(l - 1);
}

vector<ll> alls; 

int find(ll x) {
    int l = 0, r = alls.size();
    while (l < r){
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;
}


ll arr[4][maxn];
ll sum[4][maxn];
ll s2[maxn]; 
int main(){

	io;
	ll ans = 0;
	cin >> n >> k;
	for(int i = 1; i<=3; i++){
		for(int j = 1; j <= n; j++){
			cin >> arr[i][j];
			sum[i][j] = sum[i][j-1] + arr[i][j];
		}
	}
	for(int i = 1; i <= n; i++){
		s2[i] = sum[2][i]-sum[3][i-1];
		alls.push_back(s2[i]);
	}
	sort(alls.begin(), alls.end()); 
	alls.erase(unique(alls.begin(), alls.end()), alls.end());
	for(int i = 1; i <= n; i++){
		add(find(s2[i]), 1);
	}
	for(int i = 1; i <= n; i++){
		ll sum1 = sum[1][i] - sum[2][i-1] + sum[3][n];
		ans = (ans + range_ask(find(k-sum1),n) );
		add(find(s2[i]), -1);
	}
	cout << ans;
}
  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值