2019年南京大学计算机考研复试机试真题

目录

  • 第一题 Stepping Numbers
    • 题意
    • 思路
    • 代码
    • 反思
  • 第二题 Nodes from the Root
    • 题意
    • 思路
    • 代码
      • 大佬的标准题解代码:
      • 菜鸡我的又费空间,又费时间,又臭又长,思路又蠢的垃圾代码:
    • 反思
  • 第三题 Distinct Subsequences
    • 题意
    • 思路
    • 代码
      • 大佬的标准题解代码:
      • 菜鸡我的又费空间,又费时间,又臭又长,思路又蠢的垃圾代码:
    • 反思
  • 结语
  • 参考资料

今天参加了群里的一个模拟训练,训练的题目是19年的南大的题,写了三个小时,啥都没写出来,一直在写第一题,倒是有些思路,但是还是没写出来,模拟训练结束后又磨了2个小时,唉,还是放弃这道题看题解了,南大路漫漫啊~~~


第一题 Stepping Numbers


题意

2019年南京大学计算机考研复试机试真题_第1张图片
给定 l , r ( 0 ≤ l ≤ r ≤ 3 e 8 ) l,r(0≤l≤r≤3e8) l,r(0lr3e8),问 [ l , r ] [l,r] [l,r]中的自然数满足下面条件的数有多少个。
条件:数字的任意相邻两位差值都恰好为1,且数字至少有两位。

思路

本题 l , r l,r l,r看上去挺大,但容易观察到, 3 e 8 3e8 3e8以内满足条件的数其实很少,那怎样知道大致有多少呢?能不能暴力搜索呢?(请不要尝试将区间内所有数字都检查一遍,复杂度太高了)

若将相邻后一位减去前一位的大小记在一个数组中,则该数组应只含 1-1,也就是除了第一位以外都有两种选择(没错,很像二进制!),而第一位只受区间大小限制。

这样,我们就能大致估测满足条件的数不超过 10 × 2 8 ( < 1 0 4 ) 10×2^8(<10^4) 10×28(104),因此不会超时。很明显,能够直接暴力构造 (注意是暴力构造,而不是纯暴力)

当然暴力也有好几种,不管是一顿for循环还是不断dfs,都是高效的。

扩展:简单的采用记忆化的化就能处理 r 小 于 等 于 1 0 1 e 5 r 小于等于 10^{1e5} r101e5数量级的问题了(简称数位DP

代码

#include "bits/stdc++.h"

using namespace std;

int p[10];
int n,m;

int dfs(int pos,int pre,int cur){
	//pos代表当前是数字中的哪一位,pre代表这一位前一位的数字,cur代表当前这一位的前缀的大小,比如1234,2的cur的大小就是1000,3就是1200. 
	
	//res存储的是在以cur前缀下,有多少Stepping Number 
	int result = 0;
	
	/*递归边界:
		如果递归到达了个位数后面的数字,那么进行判断,如果这个数字cur在要求范围内,则加1,否则返回0 
	*/
	if(pos == -1){
		//这个条件判断式设计的很好 
		if(cur >= max(n,10) && cur <= m){
			result = 1;
			return result;
		}else{
			result = 0;
			return result;
		}
	}
	
	//当前前缀超了m?那就进行剪枝 
	if(cur > m){
		result = 0;
		return result;
	}
	
	//如果当前的前缀的大小是0~,那么这个位置就是自定义的 
	if(cur == 0){
		for(int i=0;i<=9;i++){
			result += dfs(pos-1,i,i*p[pos]);
		} 
	}else{
		//如果当前的前缀的大小不是0~,那么说明这里不能乱放数字,要根据前一位进行判断 
		
		//后面减前面为-1的情况 
		if(pre > 0){
			result += dfs(pos-1,pre-1,cur+(pre-1)*p[pos]);
		} 
		
		//后面减前面为1的情况
		if(pre < 9){
			result += dfs(pos-1,pre+1,cur+(pre+1)*p[pos]); 
		} 
	} 
	
	return result; 
 
}

int main(){
	p[0] = 1;
	for(int i=1;i<=10;i++){
		p[i] = p[i-1] * 10;
	} 
	
	int t;
	scanf("%d",&t);
	
	while(t--){
		scanf("%d %d",&n,&m);
		//我们这里的数据的范围是3e8,所以我们设置最高位是10e8,曲线救国,再加一个前缀0,即像这样 "0 _ _ _ _ _ _ _ _ _" 
		printf("%d\n",dfs(8,0,0));
	}
} 


反思

  1. 由于许多同学不太了解评测环境1s只能运算1e8~1e9次(大部分评测环境),导致第一题的大量超时提交,交题之前应先计算复杂度,这是以后需要注意的。
  2. 除了时间限制,还需要注意内存限制,32M空间限制不要开超过1000W的int变量(C++),其它空间限制可自行计算。
  3. 我自己看到这道题的第一感觉是使用动态规划,因为读了题目之后感觉有不断细化成子问题的味道在里面,但是最后弄了半天也没有弄出来。。。
  4. 看了大佬的题解,他通过分析,发现可以直接从最高位(即 1 0 8 10^8 108)开始,向着低位出发,直到最后一位来构建出所有的stepping numbers,而且他也欲判过不会超时。
  5. 关于递归,DFS,动态规划的关系
    • DFS可以用递归实现,也可以不用递归实现;
    • 动态规划可以用DFS实现,此时的动态规划称为记忆化搜索,刚好这个搜索对应了DFS中的"S".动态规划一般用递推for循环实现,至少<算法笔记>是这样实现的.
  6. 这个题目貌似真的可以用动态规划去实现,来减少DFS过程中的重复操作,后面再看吧.
  7. 大佬的代码实在是太简洁了,搞得我看了半天都没看懂…菜是原罪…
  8. 学会了使用include "bits/stdc++.h"
  9. 注意dfs()函数的涵义:返回某前缀下所有的stepping number数,因此算是划分了子问题.

第二题 Nodes from the Root


题意

在这里插入图片描述2019年南京大学计算机考研复试机试真题_第2张图片
给定一棵带边权树(原题是二叉的,加强一下数据啦,考察邻接矩阵知识), n ( n ≤ 2 e 4 ) n(n≤2e4) n(n2e4)个节点,边权不大于 1 e 7 1e7 1e7,然后给定一个 Y ( 1 ≤ Y ≤ n ) Y(1≤Y≤n) Y(1Yn),求最小的 X ( X ≥ 0 ) X(X≥0) X(X0).

X X X表示边权小于 X X X的边都会被关闭, Y Y Y表示关闭这些边以后从根节点能到达的点的数量不超过 Y Y Y.

思路

容易想到,对某一个结点,若从根结点到它的简单路径上至少有一条边被关闭,那么它就是无法到达的点。那么,如何得到这条简单路径的信息呢?

从根结点做一遍dfs就可以了!更进一步,我们想要的是最小化 X X X,因此我们先找到要关闭某个结点所需要的最小代价,这在dfs的过程中就能完成(取路径上最小的边权)。(时间复杂度为 O ( n ) O(n) O(n)

最后,只需要把这些代价存放到一个数组mi里面,从小到大排个序(当然用sort啦),然后直接输出mi[n-Y]+1就好了,复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)(因为sort的时间复杂度是这个)。

代码

大佬的标准题解代码:

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

const int maxn = 2e4+7;

int n, Y;
int head[maxn], to[maxn*2], w[maxn*2], nxt[maxn*2], tot; //更常见的是用vector存边
int mi[maxn];

inline void add_edge(int u, int v, int c) {
    ++tot; to[tot]=v; w[tot]=c; nxt[tot]=head[u]; head[u]=tot;
    ++tot; to[tot]=u; w[tot]=c; nxt[tot]=head[v]; head[v]=tot;
}

void dfs(int u, int f, int m) {
    mi[u]=m;
    for(int i=head[u]; i; i=nxt[i]) { //采用自己喜欢的遍历方式即可
        int v=to[i]; if(v==f) continue;
        dfs(v,u,min(m,w[i]));
    }
}

int main() {
    int T; scanf("%d", &T);
    while(T--) {
        scanf("%d%d", &n, &Y);
        for(int i=1; i<n; ++i) {
            int u, v, c;
            scanf("%d%d%d", &u, &v, &c);
            add_edge(u+1,v+1,c); //采用自己喜欢的连边方式即可
        }
        dfs(1,0,1<<30);
        sort(mi+1,mi+1+n);
        if(n-Y==0) printf("0\n"); //这里特判一下
        else printf("%d\n", mi[n-Y]+1); //+1是因为题目要求严格小于
        
        for(int i=1; i<=n; ++i) head[i]=0; //多组数据别忘了初始化
        tot=0;
    }
}

菜鸡我的又费空间,又费时间,又臭又长,思路又蠢的垃圾代码:

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

const int maxn = 2e4+10;

struct node{
	int v;
	int w;
};

//用于存储输入的图的数量
int t; 

//用于存储边和数量 
int n,y;

//用于存储输入的边和权重
int u,v,w; 

//邻接表存储一个结点连接的边和权重,需要每次初始化 
vector<node> adj[maxn]; 

//储存每个结点的孩子的数量,不需要每次初始化 
int childNum[maxn]; 

//存储权重的集合
set<int> st;

//存储权重的数组
int weights[maxn]; 

//用于表示下标是否被访问
bool vis[maxn]; 

 
int countChildNum(int root,int f){
	//f表示父亲结点的编号
	
	//初始化为1 
	int number = 1;
	
	//这里面自己蕴含了递归边界!!! 
	for(int i=0;i<adj[root].size();i++){
		if(adj[root][i].v == f){
			//如果这个结点是自己的父节点,则直接下次循环 
			continue;
		}else{
			number += countChildNum(adj[root][i].v,root); 
		}
	}
	
	childNum[root] =  number;
	
	return number; 
}

int countNodes(int x){
	//输入x下,根结点所拥有的结点
	//采用层序遍历进行计算
	int number = childNum[0];
	
	//每次进来都要初始化,之前因为这个一直没过题
	fill(vis,vis+maxn,false);
	
	queue<int> q; 
	q.push(0);
	vis[0] = true;
	
	while(!q.empty()){
		int cur = q.front();
		q.pop();
		//设置已经访问 
		vis[cur] = true;
		
		for(int i=0;i<adj[cur].size();i++){
			if( vis[adj[cur][i].v] == true ){
				continue;
			}else if( (adj[cur][i].w) < x ){
				//小于x,则进行删除
				number -= childNum[adj[cur][i].v]; 
			}else{
				//入队
				q.push(adj[cur][i].v); 
			}
		}
	}
	
	return number;
}

int search(int l,int r){
	//通过二分进行查找 
	int number = 0;
	
	while(l != r){
		int mid = (l + r) / 2;
		number = countNodes(weights[mid]);
		if(number > y){
			l = mid + 1;
		}else if(number <= y){
			r = mid;
		} 
	}
	
	if(l==0){
		return 0;
	}else{
		return weights[l-1]+1;
	}
}

int main(){
	scanf("%d",&t);
	
	while(t--){  
		scanf("%d %d",&n,&y);
		
		//注意一些共用的变量每次使用要进行初始化
		for(int i=0;i<n;i++){
			adj[i].clear();
		}
		st.clear();
		
		//读入数据 
		for(int i=0;i<n-1;i++){
			scanf("%d %d %d",&u,&v,&w);
			node n1;
			node n2;
			n1.v = v;
			n1.w = w;
			n2.v = u;
			n2.w = w;
			adj[u].push_back(n1);
			adj[v].push_back(n2);
			
			st.insert(w);
		}
		
		//dfs计算每个结点的孩子结点数量
		countChildNum(0,-1);
		
		int k=0;
		for(set<int>::iterator it = st.begin();it != st.end();it++){
			weights[k++] = *(it);
		}
		
		//二分查找最佳的x
		int ans = search(0,k-1);
		
		printf("%d\n",ans);	
	}
} 

反思

  1. 像这种多组测试数据一次性输入时,不要忘记每次循环都要对共享的数据进行初始化

大佬代码反思:

  1. 我与大佬的区别应该就是大佬会想到使用一次dfs()找到每个结点不可达的临界条件,然后后面直接使用一个数组把这些临界条件存储起来。再排个序,这样直接可以得到结果,非常快,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
  2. 使用了链式前向星来加边和存储二叉树(这个高级了,像普通机试用用vector就好了);

菜鸟代码反思:

  1. 一开始以为u v wu就是起点,v就是终点,但是并不是这样的,题目并没有这么说,说明自己读题不仔细,想当然了;
  2. 第一次接触通过边来建立二叉树的题目,一时间不知道怎么处理。一开始我想一开始就变成有向图,但是基于1的原因,是不可能的,所以最终只能变成无向图。使用邻接矩阵,因为使用二维数组会爆炸的;
  3. 对于2的无向图,怎么变成树呢?
    其实通过在dfs时,从root结点开始遍历目标结点就自然而然形成树了,只需要加入一些限制条件,防止结点访问它的父结点即可,你可以使用一个记录是否已访问的bool数组(需要注意的是,每次dfs之前都要进行初始化!!!),也可以把父亲结点的下标传给子节点,我写的时候太混乱了,两种都用了。
  4. 对变量的范围的理解。每一个变量都有自己的取值,写题目的时候要考虑范围的边界值,比如这里 Y Y Y等于0和N时,就要好好考虑一下!!!之前忽略了,做题的习惯还是不行;
  5. 时间复杂度分析,时间复杂度应该是 3 ∗ n l o g 2 n 3*nlog_2n 3nlog2n(因为有左右孩子还有父亲结点,所以有个3)吧,不知道分析错没有。。。
  6. 综上,发现我自己虽然会一些算法思想,但是在发现题目中的规律的能力不足,发掘不了深层次的规律,读题也不认真,容易想当然。

第三题 Distinct Subsequences

题意

2019年南京大学计算机考研复试机试真题_第3张图片
给定两个串 S , T S,T S,T ∣ S ∣ , ∣ T ∣ ≤ 1 e 4 |S|,|T|≤1e4 S,T1e4 T T T串每一个字符都是随机得到的),问 S S S串中有多少个子序列等于 T T T

要求答案对 1 e 9 + 7 1e9+7 1e9+7取模,原题其实是保证了答案不爆 i n t int int的,但由于造数据的时候很难保证答案在不爆 i n t int int的情况下还足够的强(你们懂的,数据不强容易被各种暴力做法莽过去),因此造数据的时候就造得尽可能大,但是不太清楚大家是否都了解取模的规则(离散数学里面应该学了一点的QAQ)。(我还真不知道怎么取模,我记得我离散学过呀,我记得我那时就不会。。。还有我不懂module是取模的意思。。。

这里用到的取模知识是:

(a + b) % p = (a % p + b % p) % p (1)   
(a - b) % p = (a % p - b % p) % p (2)   
(a * b) % p = (a % p * b % p) % p (3)   
ab % p = ((a % p)b) % p (4)

思路

这题不能直接使用 n 2 n^2 n2进行暴力dp求解,因为评测环境1s只能运算1e8~1e9次(大部分评测环境),使用 n 2 n^2 n2的话会超时。

动态规划常见有两种用途,一种是最优化方案,另一种就是统计方案数。如果分别用一句话来描述这两种用途的特点,我会这样描述:

  • 最优化方案:将多个子问题的方案取最优的那一个作为代表,去更新后续答案。
  • 统计方案数:将所有对后续子问题相同影响的方案放在一起,去更新后续答案。

2019年南京大学计算机考研复试机试真题_第4张图片

2019年南京大学计算机考研复试机试真题_第5张图片

代码

大佬的标准题解代码:

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

const int maxn = 1e4+7;
const int mod = 1e9+7;

char s[maxn], t[maxn];
int pos[26][maxn], cnt[26]; //pos[i][j]记录字母'a'+i在T串上的所有位置(递增排列),cnt则记录数量
long long dp[maxn];

int main() {
    int T; scanf("%d", &T);
    while(T--) {
        scanf("%s%s", s+1, t+1);
        int n=strlen(s+1);
        int m=strlen(t+1);
        memset(cnt,0,sizeof(cnt));
        for(int i=1; i<=m; ++i) dp[i]=0;
        dp[0]=1;
        for(int i=1; t[i]; ++i) {
            int c=t[i]-'a';
            cnt[c]++; //记录数量
            pos[c][cnt[c]]=i; //记录位置
        }
        for(int i=1; s[i]; ++i) { //一位一位的枚举
            int c=s[i]-'a';
            for(int j=cnt[c]; j; --j) { //从后往前枚举这个字母在T串的所有位置
                dp[pos[c][j]]=(dp[pos[c][j]]+dp[pos[c][j]-1])%mod;
            }
        }
        printf("%lld\n", dp[m]);
    }
}

菜鸡我的又费空间,又费时间,又臭又长,思路又蠢的垃圾代码:

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

const int maxn = 1e4 + 10;
const long long mod = 1e9+7;
const int maxm = 130;

typedef long long ll;

//s数组,这个string数组就很灵性,没说是字母数组哦 
char s[maxn];
//s数组的长度
int slen;
 
//t数组 
char t[maxn];
//t数组的长度
int tlen; 

//输入测试用例数
int Q;

//dp数组
//dp[i][j]表示在s的第i位,满足后缀第j位的数量
//由于是dp,所以,只要计算当前的就好了吧
//注意数值可能很大,使用long long 
ll dp[maxn];

//用于保存映射,记录每个字符在哪个位置 
vector<int> mp[maxm];

int main(){
	scanf("%d",&Q);
	while(Q--){
		//初始化dp数组
		fill(dp,dp+maxn,0); 
		//初始化映射数组 
		for(int i=0;i<maxm;++i){
			mp[i].clear();
		}
		
		//这一位设为1 
		dp[0] = 1;
		
		//记录当前哪里为之前的
		int idx = 0; 
		int curidx;
		
		s[0] = 'a';
		t[0] = 'a'; 
		scanf("%s",s+1);
		scanf("%s",t+1);
		
		//居然有点忘记strlen怎么写了 
		slen = strlen(s) - 1;
		tlen = strlen(t) - 1;			
		
		//将t的各个字符的位置读取到mp中 
		for(int i=1;i<=tlen;++i){
			int num = t[i];
			mp[num].push_back(i);
		}
		
		int cur;
		
		for(int i=1;i<=slen;++i){
			//获取当前字符 
			cur = s[i];
		
			for(int j=mp[cur].size();j>0;--j){
				int curpos = mp[cur][j-1];
				dp[curpos] = (dp[curpos-1] + dp[curpos]) % mod ;
			}	
			
		}
		printf("%lld\n",dp[tlen]);
	}
	return 0;
} 

反思

  1. 连单词 “module” 是 “取模”的意思我都不知道;
  2. 注意要使用long long,我终于学会分析数据的大小啦;
  3. dp数组降为一维很重要~~~;
  4. 这道题帮我重新回顾了取模的一些知识,希望以后有用;
  5. 这道dp题对我来说不太难,至少最后我的思路和代码和大佬基本差不多啦,有点小激动;
  6. 刚那道题的时候,心想自己在算法笔记上好像见过这个知识点,然后自己记不起来了。最后写完发现,我以为的那个其实是KMP算法,跟这个没有啥关系;
  7. 最后有个小疑问,为啥大佬的代码里面确定这道题的字符串就是小写字母呢。。。

结语

终于磨了5天把三道题写完了,撒花,这周还有,继续冲,加油(每天还要上课滴,学习不能丢~~~),我感觉这次南大的题不像我在PAT做的大多题那样直接套模板就好了,它是有一些考验智商的,比如第一题和第二题。。。我是真的没想到,只会套板子。。。

最后,树,DFS和动态规划是这次机试的考察重点!

参考资料

  1. 【赛题网站】2019南京大学计算机考研复试机试题
  2. 【题解来源】2019南京大学计算机考研复试机试题分享

你可能感兴趣的:(保研机试准备)