目录
动态规划
最长连续公共子序列
原题链接: 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;
}
二进制枚举
代码:
#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)。
- 首先,我们得到
n = 4
(数字串长度)和k = 2
(插入乘号的数量)。 - 接下来,进入循环
for (int i = 0; i < (1 << cnt); i++)
,枚举插入乘号的情况。共有1 << cnt
种情况,即1 << 3 = 8
种情况。 - 对于每种情况,统计插入乘号的数量 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
。
- 当
注意:
(i >> j) & 1
:判断 i 的第 j 位是否为1.- 不能直接 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
对结果进行位与运算,提取出最低位的值(0
或 1
)。这样可以统计出在当前二进制数 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'
最终,通过该位运算处理,可以得到所有插入乘号的情况并计算乘积。
比如[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 = 10
、r = 100
为例,说明计算过程:
初始化统计个数
ans
为 0。执行main里的for循环,枚举第一位数,从 1 开始进行深度优先搜索。
当前数为 1,满足条件
1 >= l && 1 >= 10
不成立,跳过。当前数为 2,满足条件
2 >= l && 2 >= 10
不成立,跳过。...当前数为 10,满足条件
10 >= l && 10 >= 10
成立,统计个数加一,ans = 1
。继续从 11 开始进行深度优先搜索。
当前数为 11,满足条件
11 >= l && 11 >= 10
成立,统计个数加一,ans = 2
。当前数为 110,超出范围
[l, r]
,结束递归。当前数为 111,超出范围
[l, r]
,结束递归。...
继续从 12 开始进行深度优先搜索。
当前数为 12,满足条件
12 >= l && 12 >= 10
成立,统计个数加一,ans = 3
。当前数为 121,超出范围
[l, r]
,结束递归。当前数为 122,超出范围
[l, r]
,结束递归。...
继续从 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。
- 替换:将
word1
的第i
个字符替换成word2
的第j
个字符,所需的操作次数为dp[i-1][j-1]
。 - 删除:将
word1
的第i
个字符删除,然后将前i-1
个字符转换为word2
的前j
个字符,所需的操作次数为dp[i-1][j]
。 - 插入:将
word2
的第j
个字符插入到word1
的第i
个字符之后,然后将前i
个字符转换为word2
的前j-1
个字符,所需的操作次数为dp[i][j-1]
。
举例:第一层循环 i = 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
。 - 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。 - 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
个字符所需的最少操作次数。
- 表格的第一行和第一列分别表示将空字符串转换为
word2
和word1
的操作次数。 - 每个内部单元格
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
分析:一个点/连通块不在集合内,则加入集合。
并查集基本操作:
- 查找a、b点是否在同一个集合,即,根节点是否相同:find(a) == find(b)
- 两个集合合并。合并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。
二分查找
整数序列
很多整数可以由一段连续的正整数序列(至少两个数)相加而成,比如
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;
}
- 定义两个指针
left
和right
,分别表示序列的起点和终点。 - 初始化
left
为1,right
为2。sum
记录当前序列的和。 - 进入循环,判断
sum
与目标值n
的关系:- 如果
sum
小于目标值n
,则将right
右移一位,并将sum
加上新加入的元素。 - 如果
sum
等于目标值n
,则输出当前序列,并将right
右移一位,继续寻找下一个序列。 - 如果
sum
大于目标值n
,则将left
右移一位,并将sum
减去左边界的元素。
- 如果
- 循环结束后,如果没有找到符合条件的序列,则输出"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
是根据等差数列求和公式得出的。对于从 i
到 mid
的连续序列,可以看作是一个以 i
为首项,以 mid
为末项的等差数列。
等差数列的求和公式为 Sn = (a1 + an) * n / 2
,其中 Sn
表示等差数列的和,a1
表示首项,an
表示末项,n
表示项数。
在这段代码中,i
对应 a1
,mid
对应 an
,而连续序列的项数为 mid - i + 1
(加1是因为包括了首项和末项之间的所有项)。
字符串操作
字符串的展开
题目描述
如果在输入的字符串中,含有 类似于“d-h”或者“4-8”的字串,我们就把它当作一种简写,输出时,用连续递增的字母获数字串替代其中的减号,即,将上面两个子串分别输出为 “defgh”和“45678”。在本题中,我们通过增加一些参数的设置,使字符串的展开更为灵活。具体约定如下:
(1) 展开:在输入的字符串中,出现了减号“-”,减号两侧同为小写字母或同为数字,且按照ASCII码的顺序,减号右边的字符严格大于左边的字符。
(2) 参数p1:展开方式。
- p1=1时,对于字母子串,填充小写字母;
- p1=2时,对于字母子串,填充大写字母。这两种情况下数字子串的填充方式相同。
- 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;
}
- 先生成小于等于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。
- 之后,在主函数中计算前缀和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]
- 最后,循环读取测试数据,对于每组数据,输出[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,使其从高位到低位排列。
参考: