【计蒜客】ACM程序设计课程-快速提升代码能力

目录

前言

正文

1.a+b问题

2.斐波那契数列

3.矩阵旋转

4.最大子阵

5.四平方和

6.A+B问题

7.A*B问题

8.蒜头君的随机数

9.交叉排序

10.进制转换

11.回文数

12.机器人

13.表达式求值

14.HZF爱斗牛

15.显示屏输出

16.得到整数X

17.幼儿园买玩具

18.islands打炉石传说

1.什么时候采用二进制枚举?

2.个人对于二进制枚举的理解?

后话


前言

2019年4月6日,终于赶上了大佬们一周以前的刷题进度。算是一个小小阶段的结束,因此将这一部分的题目总结一下,也算是做一个ACM's milestone。


正文

1.a+b问题

没什么好说的,权当是测试代码。

2.斐波那契数列

在常规的斐波那契基础上取余,为了防止溢出,对每一步的结果都取余即可。

3.矩阵旋转

二维数组存储数据,输出的时候推导旋转后的矩阵的行、列坐标与原矩阵的坐标的关系直接输出即可。注意输出格式,行尾不要有多余的space,要不然会“格式错误”(计蒜客真的很严格,手动滑稽)。

4.最大子阵

这应该是前几道题里比较难的一道了。我的做法是DP+降阶。

大的思路是将矩阵的二维数组,对行或列求和累加,转换为一位数组,然后对一位数组求最大子集即可。

求一位数组最大子集的时候进行DP,状态无非两种,当下一个元素为正数,累加(子集继续扩大);否则,子集重新计算求和。然后不断比较当前的和与最大值之间的大小,取最大值即可。

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


int dp[55][55];

int maxsub(int a[],int n)    
{
    int i,max=-0x3f3f3f3f,b=0;// 注意max的初始值,防止数组全为负数
    for(i=0;i<n;i++)
    {
        if(b > 0)
            b += a[i];
        else 
            b = a[i];
        if(b > max)
            max = b;
    }
    return max;
}


int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=0; i<n; i++){
		for(int j=0; j<m; j++){
			cin>>dp[i][j];
		}
	}

	int arr[55];
	int result = -0x3f3f3f3f;
	for(int i=0; i<n; i++){ // i为开始累加的行的起点 
		memset(arr,0,sizeof(arr));// i变化->子阵大小变化 
		for(int j=i; j<n; j++){ // 从第i行开始视为一个子阵 
			for(int k=0; k<m; k++){ 
                        // arr[k]表示第j行前k个元素的和 
				arr[k] += dp[j][k];
			}
			int max = maxsub(arr,m);
			if(max > result){
				result = max; 
			}
		}
	}
	cout<<result;
	return 0;
}

5.四平方和

枚举。以题目给定的数据范围,如果直接暴力四层循环嵌套,有一组数据是会超时的。因此,可以从遍历的范围和循环的层数两方面来进行优化。

遍历范围:题目要求给定的是“字典序”输出,那么后一个循环变量的起点可以是前一个循环变量。这样既减少了循环的次数,也能保证结果是按字典顺序;如果仅优化起点,这还是远远不够的,因此,每层循环的终点也要进行优化。缩小范围的方式也很简单:对于每层循环的终点,即为给定正整数N减去已经确定的变量的平方的和。例如,最外层循环变量a的起点为0,终点应该为sqrt(N),因为此时a已经确定,最差的情况是其余三个数都取0;第二层循环变量b的起点为a,保证字典序,终点为sqrt(N-a*a),因为a和b都已经确定,最坏的情况是其与两个数取0,c和d的范围以此类推。

遍历层数:当遍历到c的时候,完全可停止遍历,因为a,b,c,d和N之间存在等式,可以用计算的方式求的d的取值,然后代入等式验证即可。但要注意d应该也为整数,可以先对差开方,然后相乘验证是否相等,以此判断是否为整数。

PS感谢lxdl的指点。

6.A+B问题

大数加法。因此只需模拟竖式运算的过程:先将所有对应位置的数字相加,然后再次遍历处理进位。

需要注意的点是数据类型的转换、数字存储顺序、以及最高位是否有进位(改变输出结果的长度)。

7.A*B问题

大数乘法。跟第6题相似的处理方法,模拟竖式计算。不同的是乘法需要二维矩阵来模拟竖式。

提交时有一组用例没过,原因是乘数中有一个为0,而输出结果中把所有的0都输出了。

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

char num1[1000],num2[1000];
int numa[1000],numb[1000],ans[1000][1000],res[1000];

int main() {
	
	// 输入数字 
	scanf("%s",num1);
	getchar();
	scanf("%s",num2);

	// 数字长度 
	int la = strlen(num1);
	int lb = strlen(num2);

	// 字符转数字,并倒序存放 
	int t=0;
	for(int i=la-1; i>=0; i--) {
		numa[t++] = num1[i]-'0';
	}

	t = 0;// 别忘初始化 
	for(int i=lb-1; i>=0; i--) {
		numb[t++] = num2[i]-'0';

	}

	// 模拟竖式运算过程 
	int p=0,q=0;
	for(int i=0; i<la; i++) {
		for(int j=0; j<lb; j++) {
			ans[p][q++] = numa[i]*numb[j];
		}
		p++;
		q = 0;
		q += i+1; // 相当于乘法里的进位 
	}

	int max = la>lb?la:lb; 
	
	// 竖式的累加 
	for(int j=0; j<la+lb-1; j++) {
		for(int i=0; i<max; i++) {
			res[j] += ans[i][j];
		}
	}
	
	int len = la+lb-1; // 结果的长度 
	bool flag = false; // 最高位进位标志 
	
	// 处理进位 
	for(int j=0; j<la+lb-1; j++) {
		if(res[j]>=10) {
			res[j+1] += res[j]/10;
			res[j] = res[j]%10;
			// 最高位有进位 
			if( j==la+lb-2) {
				flag = true;
			}
		}

	}

	// 结果长度+1 
	if(flag) {
		len++;
	}

	//最高位是0,说明乘数中有一个为0,直接输出结果 
	if(res[len-1]==0) {
		cout<<"0";
	} else {
		for(int j=len-1; j>=0; j--) {
			cout<<res[j];
		}
	}

	return 0;
}

8.蒜头君的随机数

我怀疑这题就是为了C++的STL准备的。

set<int> 在输入数据的时候自动去重和从小到大排序了,所以输入完后,直接输出set<int>里的元素个数和具体元素就好。

C++中的std::set自定义去重和排序函数 https://www.cnblogs.com/litaozijin/p/6665595.html

9.交叉排序

同STL,输入完数据之后,直接sort(),注意下标关系即可。

C++中的sort函数使用总结 https://www.cnblogs.com/TX980502/p/8528840.html

10.进制转换

十进制转任意进制。对于十进制转R进制数,直接模拟模R取余法即可实现转换。

11.回文数

直接模拟题意即可,不同的点是要输出每一步的步骤。将判断回文单独拿出来写成函数,我采用的方法是将数字的各个位数分离存到字符数组中,然后首尾两个指针向中间遍历判断。数据范围很友好。

12.机器人

一开始我以为是单纯的计算坐标的问题,但是仔细读几遍题之后发现,这个题的麻烦点在于机器人有“方向”,因此要实现方向和坐标变化之间的对应关系。解决的方法是将指令的方向转化为“需要在当前方向旋转的角度”,因为题目中只给了四个方向,因此可以将角度用4个数字代替,这样数字的含义就是“与初始方向所成的夹角(统一顺时针或者逆时针)”,读入新的指令之后,只需将指令转化为需要旋转的角度,然后转化为数字累加到当前的数字之上(若越界,则取模,实际意义是转过了一圈之后相当于角度-360°)。

这是我第一次遇到这种方法,有趣而且巧妙。

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

int nowx=0,nowy=0;

int main(){
	int N;
	cin>>N;
	string direction;
	int dir=0; // 旋转角度 1代表顺时针旋转90°
	int x;
	while(N--){
		cin>>direction>>x;
		if(direction == "forward"){
			dir = dir;
		}
		if(direction == "right"){
			dir = (dir+1)%4;
		}
		if(direction == "back"){
			dir = (dir+2)%4;
		}
		if(direction == "left"){
			dir = (dir+3)%4;
		}
		
		switch(dir){
			case 0:
				nowx += x;
				break;
			case 1:
				nowy -=	x;
				break;
			case 2:
				nowx -= x;
				break;
			case 3:
				nowy += x;
				break;	
		}
	}
	cout<<nowx<<" "<<nowy;
	return 0;
} 

13.表达式求值

stack的典型应用。

设立一个数字栈和一个运算符栈,扫描表达式之前在运算符栈内放入开始符,并在表达式串后加结束符(同一个)作为标记:

数字入栈

运算符与运算符栈顶元素比较优先级:

        若当前运算符优先级大于栈顶运算符优先级,入栈;

        否则栈顶运算符出栈,并与数字栈栈顶两个元素运算,将运算结果放入数字栈中,再次比较,直至遇到结束符。

对于比较运算符优先级,可以单独封装成一个函数。

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

stack<char> ch;
stack<long long> n;

long long ans;
int len;

int priority(char si) {
	int big = 0;// 当前运算符优先级大
	
	char topch = ch.top();

	if(si == '+' && topch =='#') {
		big = 1;
	}
	if(si=='*' && topch =='#') {
		big = 1;
	}
	if(si=='*' && topch=='+') {
		big = 1;
	}

	return big;
}



int main() {
	string s;
	cin>>s;
	ch.push('#');
	//cout<<"s--->"<<s<<endl;
	int num1,num2;
	int temp =0;
	char topch;
	int numlen = 0;
	for(int i=0; i<s.size(); i++) {
		if(s[i]>='0' && s[i]<='9') {
			temp = temp*10 + (s[i]-'0'); // 转换为数

		} else {
			
			//	cout<<temp<<" "<<endl;
			n.push(temp%10000); // 把数字入栈
			temp = 0;
			numlen = 0; // 数字长度初始化
			
			// 遇到运算符, 如果当前优先级大于栈顶字符优先级,入栈
			// 如果当前优先级小于等于栈顶运算符优先级,栈顶运算符出栈,数字栈出栈两个,运算后入栈
			// 再次判断栈顶运算符与当前运算符的关系,重复上两步 
			while(!priority(s[i]) && ch.top()!='#'){
				
				topch = ch.top();
				ch.pop();
				
				num1 = n.top()%10000;
				n.pop();
				num2 = n.top()%10000;
				n.pop();

				if(topch == '+') {
					ans = num1+num2;
				} else if( topch=='*') {
					ans = num1*num2;
				}
				n.push(ans%10000);
			}
			ch.push(s[i]);
		}

	}
	cout<<n.top()<<endl;
	return 0;
}

14.HZF爱斗牛

直接按题目条件模拟即可。

需要注意的是各种情况的优先级,优先级高的先判断,条件成立立即返回就好;对于几个数之和的问题,先判断3个数的和是不是10个的正数倍,再判断2个数的和是不是正数倍,因为当出现3个10相加的情况,会导致2个数判断已经符合条件返回了,但优先级更高的三个数还没有判断,从而WA。

15.显示屏输出

最后提交的代码438行…也是没谁了。

直接模拟。将数字从上到下拆分成5个部分。“行”方向上放大时,每个部分内部循环输出;“列”方向上放大时,将每一行看做一个整体,循环输出每一行,达到“列放大”的效果。然后直接循环,按行打印。

注意的点是字符的大小,一定要数清楚space的个数,不要弄混。

(番外:最后提交的时候,“看起来”我的输出和标准输出相同,但是还是“格式错误”,原因还是因为每行最后我多输出了一个space,计蒜客真的很严格)

【计蒜客】ACM程序设计课程-快速提升代码能力

16.得到整数X

二进制枚举

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

int num[25];

int main(){
	int n,X;
	cin>>n>>X;
	for(int i=0; i<n; i++){
		cin>>num[i];
	}
	int sum = 0;
	int ans = 0;
	for(int i=0; i<(1<<n); i++){
		// i对应的是二进制状态 
		// 共有000..0(n位)~111..1(n位)2^n种状态
		// 每种状态中,1代表选中,0代表不选 
		sum = 0;
		for(int j=0; j<n; j++){
			// j为对应输入的数字的下标 
			if(i&(1<<j)){
				// 从右边开始搜索,如果是1,代表这个数字被选中 
				sum += num[j];
			}
		} 
		if(sum==X){
			ans++;
		}
	}
	cout<<ans;
	return 0;
} 

17.幼儿园买玩具

二进制枚举

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

struct s {
	int num; // 第i个小朋友想玩的玩具数量 
	int toy[15];// 玩具编号 
} kid[100];

int main(int argc, char const *argv[]) {

	int n, m, k;
	cin>>n>>m>>k;
	for (int i = 0; i < n; ++i) {
		cin>>kid[i].num;
		for(int j=0; j<kid[i].num; j++) {
			cin>>kid[i].toy[j];
		}
	}

	int max=0;
	for (int i = 0; i < (1<<k); ++i) {
		int salenum=0; // i对应状态下,玩具店出售的数目 
		int saletoy[15]; // 标记玩具店出售的玩具 
		memset(saletoy,0,sizeof(saletoy));
		
		for (int j = 0; j < k; ++j) {
			if(i&(1<<j)) {
				salenum++;
				saletoy[j]=1;
			}
			
			
			int sum = 0; // 小朋友数目 
			for (int x = 0; x < n; ++x) {
				int flag = 1;
				for (int y = 0; y<kid[x].num; y++) {
					if(saletoy[kid[x].toy[y]-1]==0) {
						// 玩具店没有小朋友想要的玩具 
						flag = 0;
					}
				}
				if(flag == 1) {
					sum++;
				}
			}

			if(sum>max && m>=salenum) {
				max = sum;
			}
		}
	}
	cout<<max<<endl;
	return 0;
}

18.islands打炉石传说

二进制枚举

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

struct card {
	int cost;  // 活力消耗
	int d;  // 卡牌种类
	int attack;  // 攻击力
} card[10];

int main() {
	
	int n;
	cin>>n;
	
	for(int i=0; i<n; i++) {
		cin>>card[i].cost>>card[i].d>>card[i].attack;
	}
	
	int max=0;
	int attacksum;//攻击力 
	int mp;  // 法力 
	bool flag;  // 是否存在随从
	 
	for(int i=0; i<(1<<n); i++) {
		
		mp = 10; 
		flag=false;  
		attacksum=0; 
		
		for(int j=0; j<n; j++) { 
			if(i&(1<<j)) {
				if(card[j].d==0){
					flag=true;
				}
				mp -= card[j].cost;
				attacksum+=card[j].attack;
			}
		}
		
		if(flag && attacksum>max && mp>=0) // 有随从&&攻击力大于max&&法力没耗尽 
			max=attacksum;
	}
	
	cout<<max;
	return 0;
}

 

连续三道题,都考查二进制枚举。(什么是二进制枚举?)

1.什么时候采用二进制枚举?

(1)题目表述中要求查询某一集合的子集,并且这些子集往往都需要满足一些条件。比如,第16题中数字集合的子集需要满足和等于X,第17题中玩具集合的子集需要满足小朋友喜欢&&不超过最大数目m,第18题中卡牌集合的子集需要满足有随从&&攻击力最大&&法力没有耗尽。

(2)变量的数据范围较小,较容易列举其状态。比如,第16题中数字集合n的取值范围为[1,20],第17题中玩具集合的元素取值范围为[1,15],第18题中卡牌集合的取值范围为[1,10]。

2.个人对于二进制枚举的理解?

所谓二进制枚举,本质还是“枚举”,也是暴力解法的一种。“二进制”决定了这种枚举方法比较适合表达状态(因为每个二进制位只有0/1)两个表示与很多种状态都刚好符合。

for(int i=0; i<(1<<n); i++){
    // code
}

循环变量i的值表示每一种“枚举状态”。(1<<n)通过位运算移位表达式表示“枚举状态”的上限,即保证遍历上限的二进制位有n位,这样就保证了i的取值可以取遍000...0(n位)~11...1(n位)的所有状态。对于每个状态i,其二进制表达中,1/0又分别对应有/无等含义,因此每个i都是一种子集的组合,通过i的遍历,就可以取遍每一个子集。

for(int j=0; j<n; j++) {
	if(i&(1<<j)) {
		if(condition) {
			// code
		}
	}
}

内层循环变量j表示“位数”,表达式(1<<j)的值只有最高位(第j+1位)为1,其余全是0,与枚举状态i按位与(&)的结果是判断i的二进制表达形式中,从右边数第j+1位是不是1,它的实际意义是“枚举状态”i所代表的子集中,从右边数第j+1位的状态(true/false)。j从0~n-1,就保证了i&(1<<j)刚好可以判断完i的每一位二进制位的状态,即判断完子集的状态。

有了子集的状态,在辅助以if(condition)进行标记,或者在一个子集的状态全部被判断完成之后,进行其他操作,便可以判断该子集是否满足要求,从而达到筛选子集的目的。


后话

终于算是勉强跟上了大佬们的脚步,也算是取得了一点点阶段性胜利,后面的路只会越来越难,应该做好付出更多、牺牲更多的准备

每个ACMer都任重道远。