408机试基础题

目录

动态规划

最长连续公共子序列

二进制枚举

字符串修改

并查集

例题

二分查找

整数序列

字符串操作

字符串的展开

切开字符串

正则表达式

数论

最大公约数和最小公倍数

斐波那契

筛素数☆


动态规划

最长连续公共子序列

原题链接: 3692. 最长连续公共子序列 - AcWing题库

输入两个字符串 s1,s2。

输出最长连续公共子串长度和最长连续公共子串。

输入:

一行,两个字符串 s1,s2,用空格隔开。

输出:

第一行输出最长连续公共子串的长度

第二行输出最长连续公共子串。如果不唯一,则输出 s1中的最后一个。

1≤|s1|,|s2||≤100
数据保证至少存在一个连续公共子串。

输入样例:

abcdefg qwercdefiok
4
cdef

思路:本来想用kmp,结果用kmp没做出来,直接暴力吧,反正也ac了没超时...注意下标

#include <iostream>
#include <string.h>

using namespace std;

const int N = 110;

char s1[N], s2[N];

int main(){
	
	cin >> s1 + 1 >> s2 + 1;
	int len = 0, start = 1;
	
	for(int i = 1; i <= strlen(s2 + 1); i++){   // 遍历长的模板子串 
		for(int j = 1; j <= strlen(s1 + 1); j++){  
			
			int l2 = i, l1 = j;   // 指针记录本次循环中的操作 
			
			while(s1[l1] == s2[l2] && s1[l1]){
				l1++, l2++;
			}
			
			// 记录已经匹配出来的,最长的s1的子串:[j, l1],长度记为len 
			if(l1 - j >= len){
				len = l1 - j;
				start = j;
			}
		}
	}
	
	cout << len << endl;
	for(int i = start; i < start + len; i++) cout << s1[i];
	
	return 0;
	 
}

二进制枚举

题目:3718. 插入乘号 - AcWing题库

代码:

#include<iostream>
using namespace std;

typedef long long LL;

int main(){
    int n, k;
    string s;
    cin >> n >> k >> s; 
    int cnt = n - 1;  
    LL res = 0;        
    
    for(int i = 0; i < 1<<cnt; i++){
    	
    	int t = 0;
    	for(int j = 0; j <= cnt; j++)
    	    t += (i >> j) & 1;
			
		if(t == k){
			
			LL p = 1;
			string str = s.substr(0, 1);
			
			for(int j = 0; j <= cnt; j++){
				if((i >> j) & 1 == 1){
				    p *= stoll(str);
				    str = s.substr(j+1, 1);
			    }else{
				    str += s.substr(j+1, 1);
			    }
			}
			p *= stoll(str);
			res = max(res, p);
	    }
	} 
	cout << res << endl;
	return 0;
}
	

时间复杂度为O(2^n),因为它使用了位运算枚举了所有可能的插入乘号的情况。空间复杂度为O(1)。

  1. 首先,我们得到 n = 4(数字串长度)和 k = 2(插入乘号的数量)。
  2. 接下来,进入循环 for (int i = 0; i < (1 << cnt); i++),枚举插入乘号的情况。共有 1 << cnt 种情况,即 1 << 3 = 8 种情况。
  3. 对于每种情况,统计插入乘号的数量 t 是否与 k 相等:

    代码中的循环 for(int j = 0; j < cnt; j++) 遍历了所有可能插入乘号的位置。通过位运算 (i >> j) & 1,可以获取当前情况下每个插入乘号位置的状态。

    在二进制表示中,每个数位代表一个位置是否插入乘号。(i >> j) & 1 的结果为 1 时,表示当前位置插入了乘号;当结果为 0 时,表示当前位置没有插入乘号

    因此,在循环内部的语句 t += (i >> j) & 1 将根据每个位置的状态,累计插入乘号的数量。通过遍历所有位置并统计插入乘号的数量,就能得到当前情况下插入乘号的总数。

    • 当 i = 0 时,二进制表示为 "000",没有插入乘号, t = 0 不等于 k,跳过。
    • 当 i = 1 时,二进制表示为 "001",在第三个位置插入乘号, t = 1 不等于 k,跳过。
    • 当 i = 2 时,二进制表示为 "010",在第二个位置插入乘号, t = 1 不等于 k,跳过。
    • 当 i = 3 时,二进制表示为 "011",在第二个和第三个位置插入乘号,此时 t = 2 等于 k。与运算可以统计1的个数:有两个1
      • 初始化 p = 1,选中的数字串为 "12"。此时 i = 011
      • 进入内层循环 for (int j = 0; j < cnt; j++)
        • 当 j = 0 时,(i >> j) & 1 的结果为 0,执行else:str += s[j + 1]。将下一个位置的数字添加到当前选中的数字后面。str = '1' + '2' = '12'
        • 当 j = 1 时,(i >> j) & 1 的结果为 1,执行if:将 当前的积 p 与指向的数字 str 相乘,得到 p = 1 * 12 = 12。同时 str 指向下一个数字3.
        • 当 j = 2 时,(i >> j) & 1 的结果为 1,执行if:将 当前的积 p 与指向的数字 str 相乘,得到 p = 12 * 3 = 36
      • for 循环出来以后,还要将最后一个数字与之前的结果相乘,p = 36 * 4 = 144
      • 更新最大结果 res = max(res, p)。
    • 继续枚举其他的情况...
    • 最终,输出最大结果 res 的值,即 2

注意:

  1. (i >> j) & 1 :判断 i 的第 j 位是否为1.
  2. 不能直接 str = s[j+1],因为char[]字符不能直接赋给string。

使用了位运算来进行枚举和统计插入乘号的数量。

1. 枚举插入乘号的情况

for(int i = 0; i < (1 << cnt); i++)

   位移运算符 << 来对整数 1 左移 cnt 位,得到一个二进制数。这个二进制数的每一位表示在对应的位置是否插入乘号。通过循环从 0 遍历到 (1 << cnt) - 1即7,即遍历了 000~111 所有二进制数,也就是枚举了所有插入乘号的情况。

2. 统计插入乘号的数量

t += (i >> j) & 1;

使用了右移运算符 >> 和位与运算符 &(i >> j) 将二进制数 i 右移 j 位,然后 (i >> j) & 1 对结果进行位与运算,提取出最低位的值(01)。这样可以统计出在当前二进制数 i 中插入乘号的数量。

3. 获取当前选中的数字串

string str = s.substr(0, 1);

这使用了 substr 函数来截取字符串 s 的子串。s.substr(start, length) 截取从位置 start 开始长度为 length 的子串。使用 s.substr(0, 1) 来获取第一个数字作为初始选中的数字。

4. 根据插入乘号情况计算乘积

if((i >> j) & 1){ 
    p *= stoll(str); 
    str = s.substr(j + 1, 1); 
} else { 
    str += s[j + 1]; 
}

根据插入乘号的情况进行判断和操作。

  • 如果当前位置需要插入乘号(即 (i >> j) & 1 == 1),将之前的数字与当前数字相乘,并更新当前选中的数字为下一个位置的数字。
  • 如果当前位置不需要插入乘号(即 (i >> j) & 1 == 0),将下一个位置的数字添加到当前选中的数字后面。比如:1后面不插入乘号,则 str = '1' + '2' = '12'

最终,通过该位运算处理,可以得到所有插入乘号的情况并计算乘积。

题目:3710. 递进数字 - AcWing题库

 比如[1,100] 中符合条件的数字有:10,12,23,34,45,56,67,78,89,98,87,76,65,54,43,32,21.共17个。

题目给出的数据范围最多是9位数,第一位数可以是1~9,那么后面的八位数都只有2种选择。设第一位选择了q,那么第二位只有可能是 q+1 或者 q−1。符合要求的总共有 9*2^8 = 1996 个数。

 代码:

#include<iostream>
using namespace std;

typedef long long LL; 

int l, r;
int ans; 

void dfs(LL x){
    if(x > r) return;             // 当前数超出范围时,结束递归
    if(x >= l && x >= 10) ans++;  // 当前数在范围内且是两位数时,满足条件的数+1 
    
    int d = x % 10;                // 获取当前数的个位数
    if(d != 0) dfs(x * 10 + d - 1);     // 如果个位数不为0,继续向下搜索生成比当前数小1的数
    if(d != 9) dfs(x * 10 + d + 1); // 如果个位数不为9,继续向下搜索生成比当前数大1的数
}

int main(){
    int T;
    cin >> T;  
    while(T--){
    	
        cin >> l >> r; 
        ans = 0;      
        
        for(int i = 1; i <= 9; i++) dfs(i);  // 第一位数:枚举1到9
        cout << ans << endl; 
    }
    
    return 0;
}

l = 10r = 100 为例,说明计算过程:

  1. 初始化统计个数 ans 为 0。

  2. 执行main里的for循环,枚举第一位数,从 1 开始进行深度优先搜索。

    • 当前数为 1,满足条件 1 >= l && 1 >= 10 不成立,跳过。

    • 当前数为 2,满足条件 2 >= l && 2 >= 10 不成立,跳过。

    • ...当前数为 10,满足条件 10 >= l && 10 >= 10 成立,统计个数加一,ans = 1

  3. 继续从 11 开始进行深度优先搜索。

    • 当前数为 11,满足条件 11 >= l && 11 >= 10 成立,统计个数加一,ans = 2

    • 当前数为 110,超出范围 [l, r],结束递归。

    • 当前数为 111,超出范围 [l, r],结束递归。

    • ...

  4. 继续从 12 开始进行深度优先搜索。

    • 当前数为 12,满足条件 12 >= l && 12 >= 10 成立,统计个数加一,ans = 3

    • 当前数为 121,超出范围 [l, r],结束递归。

    • 当前数为 122,超出范围 [l, r],结束递归。

    • ...

  5. 继续从 13 开始进行深度优先搜索。

    • 当前数为 13,满足条件 13 >= l && 13 >= 10 成立,统计个数加一,ans = 4

    • 当前数为 130,超出范围 [l, r],结束递归。

    • 当前数为 131,超出范围 [l, r],结束递归。

    • ...

依此类推,直到遍历完所有满足条件的数。最终结果为 ans = 17,即在范围 [10, 100] 内,满足数字的任意相邻两位差值都恰好为1、且数字至少有两位的数的个数为 17。

字符串修改

题目描述
设A和B是两个字符串。我们要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有三种:

删除一个字符;
插入一个字符;
将一个字符改为另一个字符。 对任给的两个字符串A和B,计算出将字符串A变换为字符串B所用的最少字符操作次数。
输入描述
第一行为字符串A;第二行为字符串B;字符串A和B的长度均小于200。
输出描述
只有一个正整数,为最少字符操作次数。
测试样例
输入

sfdxbqw
gfdgw
输出

4

动态规划经典例题——最小编辑距离 

从字符串A"daaqerdwq"(横轴)到字符串B"afwdreqew"(纵轴),需经过最少的变换次数为8。

状态转移方程如下:

dp[i][j]=min(min(dp[i-1][j]+1,dp[i][j-1]+1),(A[j-1]==B[i-1]?dp[i-1][j-1]:dp[i-1][j-1]+1));

    由于从字符串变为空 / 空变为字符串,只有单纯的删除 / 添加操作,所以 dp[x][0] 和 dp[0][x] (第一行、第一列)对应的最少操作次数,都是相应字符串位数。
    由于字符串编号从0开始,而 dp 数组中对于字符串的处理从1开始(第0行,第0列有其他用途),所以 A[j-1] 对应的是 A中第 j 个字符,B[i-1] 同理。


    计算 dp[i][j] 需要考虑三个位置的值,并选择最小值:

  • dp[i-1][j] + 1(字符串 A[0,j-1] 已经转换为字符串 B[0,i-2],还需添加一个字符 B[i-1]。比如daaqerdwq → afwdreqew,再添加w即可)、
  • dp[i][j-1] + 1(字符串A[0,j-2]已经转换为字符串B[0,i-1],还需删去一个字符A[j-1]。比如daaqerdwq → afwdreqew,再删除q即可)、
  • dp[i-1][j-1] + (1/0)(当 A[j-1]==B[i-1] 时,可直接把 A[j-2] 转换为 B[i-2] 的操作次数:dp[i-1][j-1],看作 A[j-1] 转换为B[i-1] 的操作次数 dp[i][j],否则只需将 A[j-1] 修改为 B[i-1] 即可。比如当 A[j-1] 不等于 B[i-1] 时,即 daaqerdwq → afwdreqew,q转换为 w 即可, dp[i][j] == dp[i-1][j-1]+1;当 A[j-1] 等于B[i-1]时,即daaqerdww → afwdreqew,dp[i][j] == dp[i-1][j-1])。

代码:

#include <iostream>
#include <cstring>
using namespace std;

int minDistance(string word1, string word2) {
    int m = word1.length(); 
    int n = word2.length(); 

    // dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需的最少操作次数
    int dp[m+1][n+1];

    // 初始化边界条件
    for (int i = 0; i <= m; i++) {
        dp[i][0] = i; // 当word2为空时,将word1的前i个字符删除,所需操作次数为i
    }
    for (int j = 0; j <= n; j++) {
        dp[0][j] = j; // 当word1为空时,将word2的前j个字符插入,所需操作次数为j
    }

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1[i-1] == word2[j-1]) {
                // 当前字符相同,不需要操作,继承上一个状态的操作次数
                dp[i][j] = dp[i-1][j-1];
            } else {
                // 当前字符不同,选择最小的操作次数,并加1
                dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
            }
        }
    }

    return dp[m][n]; // 返回将word1转换为word2所需的最少操作次数
}

int main() {
    string A, B;
    cin >> A >> B; 
    int result = minDistance(A, B); 
    cout << result << endl; 
    return 0;
}

    用二维数组dp来记录最少操作次数。

    首先,初始化边界条件,即空字符串转换为另一个字符串的操作次数为其长度。当word1为空时,将word2逐个字符删除,所以dp[i][0] = i;当word2为空时,将word1逐个字符插入,所以dp[0][j] = j

    然后,使用两个嵌套循环遍历字符串A和B,通过比较字符是否相同来更新dp数组的值。

  • 字符相同时,不需要进行操作,dp[i][j]= dp[i-1][j-1]
  • 字符不同时,可以选择删除、插入或替换字符,选择其中操作次数最少的那个并加1。
  1. 替换:将word1的第i个字符替换成word2的第j个字符,所需的操作次数为dp[i-1][j-1]
  2. 删除:将word1的第i个字符删除,然后将前i-1个字符转换为word2的前j个字符,所需的操作次数为dp[i-1][j]
  3. 插入:将word2的第j个字符插入到word1的第i个字符之后,然后将前i个字符转换为word2的前j-1个字符,所需的操作次数为dp[i][j-1]

举例:第一层循环 i = 1中,

  1.  j = 1: 对于word1第一个字符's'和word2第一个字符'g',它们不相同。我们需要选择最小的操作次数,并加1。这里有三种可能的操作:替换、插入和删除。分别对应dp[i-1][j-1](替换操作)、dp[i-1][j](删除操作)和dp[i][j-1](插入操作)。取它们的最小值并加1,即dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1。 因此,dp[1][1] = min(dp[0][0], min(dp[0][1], dp[1][0])) + 1 = min(0, min(1, 1)) + 1 = 1
  2. j = 2: 对于第一个字符's'和第二个字符'f',它们不相同。同样,我们选择最小的操作次数,并加1。dp[1][2] = min(dp[0][1], min(dp[0][2], dp[1][1])) + 1 = min(1, min(2, 1)) + 1 = 2。把's'转换为'gf'=把word1前1个字母转换为word2前2个字母=2。
  3. j = 3: 对于第一个字符's'和第三个字符'd',它们不相同。同样地,我们选择最小的操作次数,并加1。dp[1][3] = min(dp[0][2], min(dp[0][3], dp[1][2])) + 1 = min(2, min(3, 2)) + 1 = 3。把's'转换为'gfd'=把word1前1个字母转换为word2前3个字母=3。

  

在上面的表格中,横轴表示word2的字符,纵轴表示word1的字符。每个单元格dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需的最少操作次数。

  • 表格的第一行和第一列分别表示将空字符串转换为word2word1的操作次数。
  • 每个内部单元格dp[i][j]的值由其左上方、正上方和左边的三个单元格的值决定。
  • 如果word1[i-1]word2[j-1]相等,则dp[i][j]的值等于左上方单元格的值,表示字符相同,无需操作。
  • 如果word1[i-1]word2[j-1]不相等,则dp[i][j]的值是三个相邻单元格中的最小值加1,分别表示替换、删除和插入操作

最终,右下角的单元格dp[m][n]表示将word1转换为word2所需的最少操作次数。

并查集

例题

原题链接: 3719. 畅通工程 - AcWing题库

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。所有道路都是双向的。

省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条双向道路?

输入:

第 1 行给出两个正整数,分别是城镇数目 N 和道路数目 M。

随后的 M 行对应 M 条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。城镇从 1 到 N 编号。

注意:两个城市之间可以有多条道路相通。

也就是说

3 3
1 2
1 2
2 1

这种输入也是合法的。

输出一个整数,表示最少还需要建设的道路数目。

1≤N≤1000,1≤M≤10000

4 2
1 3
4 3
1

分析:一个点/连通块不在集合内,则加入集合。

并查集基本操作:

  1. 查找a、b点是否在同一个集合,即,根节点是否相同:find(a) == find(b)
  2. 两个集合合并。合并a到b:a = p[b]

    核心思想:路径压缩优化:在查找一个元素 x 所在集合的根结点时,一边往上溯根,一边将这些结点的父结点都改为最终的集合根结点。一言以蔽之:儿子和父亲变成兄弟。

    无向图、稀疏图,存储用邻接表。但是这道题不用存储!因为无需遍历,只需要将所有点连通并记录次数就行。

代码:

#include <iostream>
 
using namespace std;
const int N = 10010;
 
int n, m;
int p[N]; // 存储父节点 


int find(int x){    // 基础操作:找根节点 
	if(p[x] != x) return p[x] = find(p[x]); // 路径压缩,根节点设为父节点
	return x; 
}

void merge(int a, int b){  // 基础操作:集合合并 
	a = find(a), b = find(b); 
	if(a != b) p[a] = b;   // 把x合并到y上 
}

 
int main(){
	cin >> n >> m;
	
	for(int i = 1; i <= n; i++) p[i] = i; // 记得初始化啊啊啊啊 
	
	while(m--){
		int a, b;
		cin >> a >> b;
		merge(a, b);   // 两个点合并/连通 
	}
	
	int cnt = 0;
	for(int i = 1; i <= n; i++)
		if(p[i] == i)
		    cnt++;
	
	cout << cnt-1 << endl;
	
	return 0;
} 

最重要的一步:计算需要增加几条道路:

	int cnt = 0;
	for(int i = 1; i <= n; i++)
		if(p[i] == i)
		    cnt++;

参考:AcWing 3719. $\Huge\color{gold}{畅通工程}$ - AcWing

就是计算连通块数量,根节点是自身的节点数量 = 连通块数量,最后需要增加的道路数量 = 连通块数量 -1。

二分查找

整数序列

题目链接:3717. 整数序列 - AcWing题库

很多整数可以由一段连续的正整数序列(至少两个数)相加而成,比如 

25 = 3+4+5+6+7 = 12+13。

输入一个整数 N,输出 N 的全部正整数序列,如果没有则输出 NONE

输入:

一个整数 N。

输出:

每行输出一个满足条件的整数序列。

序列内部元素从小到大排序。优先输出首项更小的序列。

2≤N≤1e7

样例:

25
3 4 5 6 7
12 13

方法一:暴力,代求和公式。使用了两层嵌套的循环来遍历可能的序列起点和长度。对于每个起点和长度,计算序列的和并进行比较,这种方法复杂度较高,导致程序运行时间过长。

#include <iostream>

using namespace std;

int main(){
	
	int n;
	bool st = false;
	
	cin >> n; 
	
	for(int i = 1; i <= n/2; i++){
		for(int j = 2; j <= n/2; j++){
			int a = i;
			
			if((a*j + j*(j - 1)/2) == n){
				st = true;   // 存在此连续序列 
				
				int k = j;
				while(k--)
					cout << a++ << " ";
				
				cout << endl;
			}	
		}
	}
	
	if(!st) cout << "NONE";
	
	return 0;
}

方法二:双指针法。

#include <iostream>
using namespace std;

int main() {
    int n;
    bool st = false;
    cin >> n;

    int left = 1, right = 2; // 序列起点和终点
    int sum = 3; // 当前序列的和,初始值为序列中的第一个元素

    while (left <= n / 2) {
        if (sum < n) {
            right++; // 右移右指针
            sum += right; // 更新序列和
        } else if (sum == n) {
            st = true;
            for (int i = left; i <= right; i++) {
                cout << i << " ";
            }
            cout << endl;
            right++; // 继续寻找下一个序列
            sum += right; // 更新序列和
        } else {
            sum -= left; // 左移左指针
            left++; // 更新序列和
        }
    }

    if (!st) cout << "NONE";

    return 0;
}
  1. 定义两个指针leftright,分别表示序列的起点和终点。
  2. 初始化left为1,right为2。sum记录当前序列的和。
  3. 进入循环,判断sum与目标值n的关系:
    • 如果sum小于目标值n,则将right右移一位,并将sum加上新加入的元素。
    • 如果sum等于目标值n,则输出当前序列,并将right右移一位,继续寻找下一个序列。
    • 如果sum大于目标值n,则将left右移一位,并将sum减去左边界的元素。
  4. 循环结束后,如果没有找到符合条件的序列,则输出"NONE"。

这样优化后的代码不再需要嵌套循环,只需线性遍历一次,时间复杂度为O(n)。

方法三:二分查找

#include <iostream>

using namespace std;

typedef long long ll; 

const int N = 1e7 + 10; 

int n;
bool st; // 标记是否找到序列

int main()
{
    cin >> n; 

    for (int i = 1 ; i <= n / 2; i++) // 遍历可能的序列起点i
    {
        ll l = i + 1, r = n / 2 + 1; 
        while(l < r) // 二分查找序列终点
        {
            ll mid = l + r >> 1; 
            long long x = (i + mid) * (mid - i + 1) / 2; // 计算序列和 
            if (x >= n) r = mid; // 如果序列和大于等于n,则缩小右边界
            else l = mid + 1;    // 否则扩大左边界
        }

        if ((i + r) * (r - i + 1) / 2 == n) // 判断当前序列是否满足条件
        {
            for (int j = i; j <= r; j++) printf("%d ", j); // 输出序列中的元素
            puts(""); 
            st = 1; // 设置标记为true,表示找到了符合条件的序列
        }
    }

    if (!st) puts("NONE"); 

    return 0;
}

这里的序列和的计算公式 (i + mid) * (mid - i + 1) / 2 是根据等差数列求和公式得出的。对于从 imid 的连续序列,可以看作是一个以 i 为首项,以 mid 为末项的等差数列。

等差数列的求和公式为 Sn = (a1 + an) * n / 2,其中 Sn 表示等差数列的和,a1 表示首项,an 表示末项,n 表示项数。

在这段代码中,i 对应 a1mid 对应 an,而连续序列的项数为 mid - i + 1(加1是因为包括了首项和末项之间的所有项)。

字符串操作

字符串的展开

题目描述
如果在输入的字符串中,含有 类似于“d-h”或者“4-8”的字串,我们就把它当作一种简写,输出时,用连续递增的字母获数字串替代其中的减号,即,将上面两个子串分别输出为 “defgh”和“45678”。

在本题中,我们通过增加一些参数的设置,使字符串的展开更为灵活。具体约定如下:

(1) 展开:在输入的字符串中,出现了减号“-”,减号两侧同为小写字母或同为数字,且按照ASCII码的顺序,减号右边的字符严格大于左边的字符。

(2) 参数p1:展开方式。

  1. p1=1时,对于字母子串,填充小写字母;
  2. p1=2时,对于字母子串,填充大写字母。这两种情况下数字子串的填充方式相同。
  3. p1=3时,不论是字母子串还是数字字串,都用与要填充的字母个数相同的星号“*”来填充。

(3) 参数p2:填充字符的重复个数。p2=k表示同一个字符要连续填充k个。例如,当p2=3时,子串“d-h”应扩展为“deeefffgggh”。减号两边的字符不变。

(4) 参数p3:是否改为逆序:p3=1表示维持原来顺序,p3=2表示采用逆序输出,注意这时候仍然不包括减号两端的字符。例如当p1=1、p2=2、p3=2时,子串“d-h”应扩展为“dggffeeh”。

(5) 如果减号右边的字符恰好是左边字符的后继,只删除中间的减号,例如:“d-e”应输出为“de”,“3-4”应输出为“34”。如果减号右边的字符按照 ASCII码的顺序小于或等于左边字符.输出时,要保留中间的减号,例如:“d-d”应输出为“d-d”,“3-1”应输出为“3-1”。

100%的数据满足:1< =p1< =3,1< =p2< =8,1< =p3< =3。字符串长度不超过100

输入描述
输入包括两行: 
第1行为用空格隔开的3个正整数,一次表示参数p1,p2,p3。 
第2行为一行字符串,仅由数字、小写字母和减号“-”组成。行首和行末均无空格。 
输出描述
输出只有一行,为展开后的字符串。 

输入

1  2  1 
abcs-w1234-9s-4zz

输出

abcsttuuvvw1234556677889s-4zz

 代码:

#include <iostream>
#include <string>
using namespace std;

// 判断字符是否为小写字母
bool isLower(char c) {
    return c >= 'a' && c <= 'z';
}

// 判断字符是否为大写字母
bool isUpper(char c) {
    return c >= 'A' && c <= 'Z';
}

// 展开字符串
string expandString(int p1, int p2, int p3, string str) {
	
    string result = "";
    int len = str.length();
    
    for (int i = 0; i < len; i++) {
        if (str[i] == '-') {
            // 如果是减号,则判断减号两侧的字符
            char left = str[i - 1];
            char right = str[i + 1];
            
            if ((isLower(left) && isLower(right)) || (isdigit(left) && isdigit(right))) {
            	
                // 如果减号两侧同为小写字母或数字
                if (p3 == 1) {
                    // 维持原来顺序
                    if (left < right) {
                        // 左边字符小于右边字符,删除中间的减号
                        if (isLower(left)) {
                            for (char c = left + 1; c < right; c++) {
                                if (p1 == 1) {
                                    // 填充小写字母
                                    result += string(p2, c);
                                } else if (p1 == 2) {
                                    // 填充大写字母
                                    result += string(p2, c - 'a' + 'A');
                                } else {
                                    // 填充星号
                                    result += string(p2, '*');
                                }
                            }
                        } else {
                            for (char c = left + 1; c < right; c++) {
                                result += string(p2, c);
                            }
                        }
                    } else {
                        // 左边字符大于等于右边字符,保留中间的减号
                        result += "-";
                    }
                } else {
                    // 逆序输出
                    if (left < right) {
                        // 左边字符小于右边字符,删除中间的减号
                        if (isLower(left)) {
                            for (char c = right - 1; c > left; c--) {
                                if (p1 == 1) {
                                    // 填充小写字母
                                    result += string(p2, c);
                                } else if (p1 == 2) {
                                    // 填充大写字母
                                    result += string(p2, c - 'a' + 'A');
                                } else {
                                    // 填充星号
                                    result += string(p2, '*');
                                }
                            }
                        } else {
                            for (char c = right - 1; c > left; c--) {
                                result += string(p2, c);
                            }
                        }
                    } else {
                        // 左边字符大于等于右边字符,保留中间的减号
                        result += "-";
                    }
                }
            } else {
                // 减号两侧不为同一类字符,保留中间的减号
                result += "-";
            }
        } else {
            // 非减号字符直接添加到结果字符串中
            result += str[i];
        }
    }
    return result;
}

int main() {
    int p1, p2, p3;
    string str;
    cin >> p1 >> p2 >> p3;
    cin >> str;
    cout << expandString(p1, p2, p3, str) << endl;
    return 0;
}

切开字符串

题目描述
定义“正回文子串”为:长度为奇数的回文子串。 设切成的两段字符串中,前一段中有A个不相同的正回文子串,后一段中有B个不相同的非正回文子串,则该方案的得分为A*B。那么所有的切割方案中,A*B的最大值是多少呢?

输入描述
输入第一行一个正整数N(<=10^5)
接下来一行一个字符串,长度为N。该字符串仅包含小写英文字母。
输出描述
一行一个正整数,表示所求的A*B的最大值。
输入

10
bbaaabcaba
输出

38

    注意子串的概念!比如abc的子串为a, ab, abc, b, bc, c;

    基本思想是:遍历字符串中每一个位置,将其分为两部分,左边统计正回文子串数目,右边统计非正回文数目,存入echoNum和notEchoNum数组中,最后遍历这两个数组求得相应位置乘积,取得最大值;

    利用集合set<string> echo, notEcho存储正回文/非正回文子串,使得子串唯一,并用数组echoNum[100010], notEchoNum[100010]存储对应位置的正回文/非正回文子串数目(根据各自集合的大小);

正回文子串:

b  b  a  a   a  b   c   a  b  a

1  1  2  2  3  4  5  5  5  6

非正回文子串:

b    b   a   a   a   b   c  a   b  a

39 30 24 19 13  9   5  2   1  0

代码:

//#include<bits/stdc++.h>
#include<iostream>
#include<algorithm>
#include<set>
#include<string>
using namespace std;
 
set<string> echo, notEcho;//当前字符串所有字串含有的回文/非回文子串
 
bool isEcho(string s){
    if(s.length() % 2 != 1){  // 长度为偶数,肯定不回文 
        return false;
    }else{
        for(int i = 0; i < s.length() / 2; i++){
            if(s[i] != s[s.length() - 1 - i]){
                return false;
            }
        }
        return true;
    }
}
 
int main(){
    int n, echoNum[100010], notEchoNum[100010];
    string input, temp;
    cin>> n >> input;
    
    for(int i = 0; i < n; i++){         // 自左向右 统计位置0到i为止 所有正回文子串数目
        for(int j = 0; j <= i; j++){
            temp = input.substr(j, i - j + 1);
            if(isEcho(temp)){
                echo.insert(temp);     // set的插入元素操作 
            }
        }
        echoNum[i] = echo.size();    // 存储对应位置的正回文子串数目 
    }
    
    for(int i = n - 1; i >= 0; i--){    // 自右向左 统计位置i到n-i为止 所有非正回文子串数目
        for(int j = i; j < n; j++){
            temp = input.substr(i, j - i + 1);
            if(!isEcho(temp)){
                notEcho.insert(temp);
            }
        }
        notEchoNum[i] = notEcho.size(); // 存储对应位置的非正回文子串数目 
    }
    
    int ans = 0;
    
    for(int i = 0; i < n - 1; i++){     // 划分的两部分均不能为空 所以只到n-1
        ans = max(ans, echoNum[i] * notEchoNum[i+1]);
    }
    
    cout<<ans<<endl;
    
    return 0;
}

正则表达式 

题目描述
给出一个非空的正则表达式和一个字符串,求该字符串是否能匹配该正则表达式。

这个正则表达式可能含有:

基本元素:

空串,输入中不体现;

单个小写字母(例如 a ),a 匹配小写字母 a,这里 a 表示小写字母 a。

运算符:

连接(例如 ab),ab 匹配可以表示成一个与 a 匹配的串与一个与 b 匹配的串相连接的串,这里 a 表示一个正则表达式,下同。

或(例如 a|b),a|b 匹配与 a 和 b 中至少一个匹配的串。

闭包(例如 a),a 匹配零个或多个与 a 匹配的串的连接。

正闭包(例如 a+),a+ 匹配一个或多个与 a 匹配的串的连接。

括号(例如 (a))

其中连接和或是二元运算符,闭包和正闭包是一元运算符。

所有运算符都是左结合的,即同等优先级的运算顺序从左到右。

闭包和正闭包的优先级最高,连接次之,或的优先级最低。

输入描述
多组数据,每组数据两行:
第一行是一个非空正则表达式,保证符合上述定义,但可能出现多余括号。保证不出现空括号。
第二行是一个由小写字母组成的非空字符串。
输出描述
对于每组数据,如果正则表达式能匹配该字符串,输出一行 "Yes",否则输出一行 "No",不含引号。

输入样例

aa
a
a*
aa
a+
a
c*a*b
aab
ab*a
bbb
a(a|b+)*a
aa
a(a|b+)*a
aababba
a(a|cb+)*a
aca
a(a|cb+)*a
acbbbaacba
a(a|cb+)*a
acbbbaacb
((a*))
aa

输出样例

No
Yes
Yes
Yes
No
Yes
Yes
No
Yes
No
Yes

数论

acwing 3642. https://www.acwing.com/activity/content/problem/content/8783/

最大公约数和最小公倍数

https://www.acwing.com/activity/content/problem/content/8783/

输入两个正整数 m 和 n,求其最大公约数和最小公倍数。

1≤n,m≤10000

5 7
1 35

代码:

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

int gcd(int m, int n){
	if(n == 0) return m;
    return gcd(n, m % n); 
}
int main(){
	int m, n;
	cin >> m >> n;
	cout << gcd(m, n) << " " << m * n / gcd(m, n);
	return 0;
}

最大公约数:辗转相除法,也可以直接调用__gcd(m , n) 函数

最小公倍数:m * n / gcd(m, n),即:两个数的乘积 / 两个数的最大公约数 = 最小公倍数

斐波那契

3643. 上楼梯

一个楼梯共有 n级台阶,每次可以走一级或者两级或者三级,问从第 0 级台阶走到第 n

级台阶一共有多少种方案。1≤N≤20.

4
7
#include <iostream>
using namespace std;

int countWays(int n) {
    int dp[n+1];
    // 初始化第0级和第1级台阶的方案数
    dp[0] = 1;
    dp[1] = 1;
    dp[2] = 2;

    for (int i = 3; i <= n; i++) {
        // 当前台阶的方案数等于前一级、前两级和前三级方案数之和
        dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
    }

    return dp[n];
}

int main() {
    int n;
    cout << "请输入楼梯的总级数:";
    cin >> n;
    int ways = countWays(n);
    cout << "从第0级台阶走到第" << n << "级台阶的方案总数为:" << ways << endl;
    return 0;
}

 思路:使用一个数组dp来记录每个台阶的方案数。初始化时,dp[0]dp[1]的值都为1,因为从第0级台阶到第0级台阶只有一种方案(不走),从第0级台阶到第1级台阶也只有一种方案(直接走一级)。而从第0级台阶到第2级台阶有两种方案,分别是走一级或者直接走两级。

然后,我们使用一个循环从第3级台阶开始计算方案数。对于每个台阶i,它的方案数等于前一级、前两级和前三级方案数之和,即dp[i] = dp[i-1] + dp[i-2] + dp[i-3]

最后,返回dp[n]即可得到从第0级台阶走到第n级台阶的方案总数。

 

筛素数☆

acwing 3701. 非素数个数

求 [a,b]之间的合数个数。注意,1既不是质数,也不是合数。

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

每组数据输出一行答案,表示合数的个数。

1≤a≤b≤107

1 10
1 100
5
74

方法1:暴力,会超时

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

bool prime(int i){
	if(i <= 1) return true;
	for(int j = 2; j <= sqrt(i); j++){
		if(i % j == 0) return false;
	}
	return true;
}

int main(){
	
	int a, b;
	cin >> a >> b;
	int cnt = 0; 
	for(int i = a; i <= b; i++){
		if(!prime(i)) cnt++;
	}
	
	cout << cnt;
	return 0;
}

方法2:埃氏筛法+前缀和

#include <iostream>
using namespace std;
const int N = 1e7+10;
bool isprime[N+1];      // 标记是否为合数,1合数,0素数 
int s[N+1];             // 前缀和,存储某个数前面所有合数的数量

int main(){
    for(int i = 2; i * i <= N; i++){
        if(!isprime[i]){
            for(int j = i * i; j <= N; j += i)   // i的所有倍数是合数
            {
                isprime[j] = true;
            }
        }
    }

    for(int i = 1; i <= N; i++){
        s[i] = s[i-1] + isprime[i];          // 计算前缀和s[i]
    }

    int a = 0, b = 0;
    while(cin >> a >> b){
        cout << s[b] - s[a-1] << endl;      // 输出[a,b]之间的合数个数,即s[b] - s[a-1]
    }
    return 0;
}
  1. 先生成小于等于N的所有素数。外层循环从2开始遍历到sqrt(N),如果当前数 i 没有被标记为合数(isprime[i]==false),则将其所有倍数(i*i, i*(i+1), ...)标记为合数(isprime[j]设为true)。比如i=2时,则4/6/8都标记为true。i=3时,则6/9都标记为true。
  2. 之后,在主函数中计算前缀和s。循环遍历从1到N的每个数i,根据isprime[i]是否为true,更新前缀和s[i],s[i]等于s[i-1]+isprime[i]。计算前缀和的公式:s[0] = a[0] ,s[i] = s[i-1] + a[i]
  3. 最后,循环读取测试数据,对于每组数据,输出[a,b]之间的合数个数,即s[b] - s[a-1]。这里注意要处理a=0的情况,因为s[0]并未被赋初值,默认为0。计算区间和公式:sum[l, r] = s[r] - s[l-1]

十六进制不进位加法

acwing 3702. 十六进制不进位加法

16进制不进位的加法,即和正常加法类似,只是不用去计算进位的数,比正常的加法更简单。如 A+6=0(正常加法是 10,但是由于不进位所以只有 0)。

输入包含多组测试数据。

每组数据占一行,包含两个十六进制数,字母统一大写。

每组数据输出一行不进位加法的结果。结果中可能包含前导 0。

输入数字长度不超过 100

 
123 456
6 A
579
0

代码:

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

string a, b;
string st = "0123456789ABCDEF"; // 用于将余数转换为十六进制字符

int main() {
    while(cin >> a >> b) { 
        int l1 = a.length(), l2 = b.length(); 
        int len = max(l1, l2);
        reverse(a.begin(), a.end()); // 反转字符串,便于从低位开始计算
        reverse(b.begin(), b.end()); 
        
        string s = ""; 
        
        for(int i = 0; i < len; i++) { // 从低位到高位遍历每一位
            int t = 0;                 // 存储当前位置的相加结果
            
            if(i < l1) {               // 如果字符串a还有未处理的位数
                if(a[i] >= '0' && a[i] <= '9') // 判断当前字符是否为数字
                    t += a[i] - '0';           // 将数字字符转换为对应的整数下标 
                else
                    t += a[i] - 'A' + 10;      // 将字母字符转换为对应的整数下标 
            }
            
            if(i < l2) { 
                if(b[i] >= '0' && b[i] <= '9') 
                    t += b[i] - '0'; 
                else
                    t += b[i] - 'A' + 10; 
            }
            
            s += st[t %= 16];     // 相加结果取模16的余数,得到不进位的和,并将其转换为十六进制字符,拼接到结果字符串
        }
        
        reverse(s.begin(), s.end()); 
        cout << s << endl; 
        
    }
    return 0;
}

思路:

定义字符串st,包含0-15的十六进制字符,用于将余数转换为十六进制字符。

取较长的字符串长度作为循环次数。使用reverse函数反转字符串a和b,使其从低位到高位排列。

使用for循环,从低位到高位遍历每一位。定义整数变量t,用于存储当前位置的相加结果。

如果字符串a还有未处理的位数(i < l1):判断当前字符是否为数字,如果是数字则将其转换为对应的整数。如果是字母,则将其转换为对应的整数。

如果字符串b还有未处理的位数(i < l2),同样进行相同的操作。

对相加结果t取模16,得到不进位的和,并使用st数组将其转换为十六进制字符,然后拼接到结果字符串s中。循环结束后,反转结果字符串s,使其从高位到低位排列。

参考:

2020南京大学软件学院夏令营模拟机试题集_&再见萤火虫&的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值