水木清研OJ:http://59.110.82.144/

Acwing:https://www.acwing.com/problem/

20250519-动态规划

最长公共子序列-模板

https://www.acwing.com/problem/content/3513/

  • 最长公共子序列的经典思路是比较最后一个元素是否相等

假设一个最优子结构情况,对于一个序列$$Z = {z_1, z_2, \ldots, z_k}$$是$$X = {x_1, x_2, \ldots, x_m}$$和$$Y = {y_1, y_2, \ldots, y_n}$$的最长公共子序列。根据对最后一个元素考虑,我们可以分成三种情况来具体讨论:

(1)$$x_m = y_n = z_k$$

  • 将$$x_m$$和$$y_n$$的值作为序列$$Z$$的最后一个元素,如果去掉$$x_m$$和$$y_n$$,那么对于序列$$Z$$的最长公共子序列的长度肯定会减小1,即$$c[i - 1][j - 1] = c[i][j] - 1$$;因此,当$$x_m = y_n$$时的递推公式是:$$c[i][j] = c[i - 1][j - 1] + 1$$;

(2)若$$x_m \neq y_n$$且$$z_k \neq x_m$$,则$Z$是$$X-x_m$$和$Y$的最长公共子序列。

(3)若$$x_m \neq y_n$$且$$z_k \neq y_n$$,则$Z$是$X$和$$Y-y_n$$的最长公共子序列。

则递推式:
对于$$c[i][j]$$数组:指只取$$X$$前$$i$$个元素和只取$$Y$$前$$j$$个元素能够得到的最长公共子序列的值(即存放最优子结构的值)。
由1)可知,如果$$x_i = y_j$$,那么可以从没有$$xi$$和$$yj$$的最优子结构即$$c[i - 1][j - 1]$$转移过来,即
$$
c[i][j] = c[i - 1][j - 1] + 1
$$
由2)3)可知,如果$$x_i \neq y_j$$,那么我们只需要取$$x_i$$和$$y_{j-1}$$最优子结构和$$x_{i-1}$$和$$y_j$$最优子结构的最大长度即可,即
$$
c[i][j] = \max(c[i][j - 1], c[i - 1][j])
$$

int main()
{
//最优子结构状态值初始化(当然也可以不写,因为全局变量默认为0)
memset(c, 0, sizeof(c));
//输入序列s1和s2的长度
cin>>len1>>len2;
//输入需要求最长公共子序列的序列s1和s2
cin>>s1+1>>s2+1;
for(int i = 1; i <= len1; i ++){
for(int j = 1; j <= len2; j ++){

if(s1[i] == s2[j])c[i][j] = c[i - 1][j - 1] + 1;
else c[i][j] = max(c[i - 1][j], c[i][j - 1]);

}
}
cout<<c[len1][len2]<<'\n';
return 0;
}

20250520-动态规划、贪心算法、STL

STL-vector

参考博客:https://blog.csdn.net/weixin_45031801/article/details/134769122

头文件

#include <vector>

或者

#include <bits/stdc++.h> 

#include <bits/stdc++.h> 头文件,它是一个非标准的头文件,通常由一些编译器(如 GCC)提供。这个头文件包含了大量的标准库头文件,包括 <vector>、<iostream>、<algorithm> 等等。

初始化

  • 方式一:构造一个某类型的空容器:
    • vector<数据类型> 函数名; 初始化为空。
vector<int> v1; //构造int类型的空容器
  • 方式二:构造一个含有n个val的某类型容器:

    • vector<数据类型> 函数名(a,b).定义a个空间,都初始化为b。
vector<int> v2(10, 2); //构造含有10个2的int类型容器
  • 方式三:拷贝构造某类型容器的复制品:

    • vector<数据类型> 函数名1(函数名2),把动态数据2复制给动态数组1
vector<int> v3(v2); //拷贝构造int类型的v2容器的复制品
  • 方式四:使用迭代器拷贝构造某一段内容:

    • vector<数据类型> 函数名1(函数名2.begin(),函数名2.end())把动态数组2复制给动态数组1。
vector<int> v4(v2.begin(), v2.end()); //使用迭代器拷贝构造v2容器的某一段内容
  • 方式五:迭代器构造函数也可以使用数组来进行构造,传的区间是左闭右开
vector<int> 函数名(a,a+sizeof(a)/sizeof(数据类型)),把普通数组a复制给动态数组。

注意:该方式也可用于拷贝其他容器的某一段内容。

string s("hello world");
vector<char> v5(s.begin(), s.end()); //拷贝构造string对象的某一段内容

示例代码

#include <iostream>
#include <vector>
using namespace std;
int main()
{
std::vector<int> first; // 构造一个没有元素的空容器
std::vector<int> second(2, 10); // 2个值为10的整数
std::vector<int> third(second.begin(), second.end()); // 迭代器构造
std::vector<int> fourth(third); // 拷贝构造

// 迭代器构造函数也可以使用数组来进行构造,传的区间是左闭右开
// 因为指向数组空间的指针是天然的迭代器
int arr[] = { 16,2,77,29 };
std::vector<int> fifth(arr, arr + 4);
// std::vector<int> fifth (arr, arr + sizeof(arr) / sizeof(int) );

// first : []
// second: [10,10]
// third : [10,10]
// fourth: [10,10]
// fifth : [16,2,77,29]
return 0;
}

访问数组元素

使用“下标+[]”的方式遍历容器

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

int main()
{
vector<int> v(5, 1);
//使用“下标+[]”的方式遍历容器
for (size_t i = 0; i < v.size(); i++) cout << v[i] << " ";
cout << endl;
return 0;
}

迭代器(指针)

接口名称 使用说明
begin() 返回指向第一个元素的迭代器
end() 返回指向最后一个元素的下一个位置的迭代器
rbegin() 返回指向最后一个元素的反向迭代器
rend() 返回指向第一个元素的前一个位置的反向迭代器

示例代码

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

int main()
{
vector<int> v(10, 2);
//正向迭代器遍历容器
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}

访问操作

接口名称 接口说明
back() 返回容器中最后的一个元素的引用
front() 返回容器中第一个元素的引用
int main()
{
int a[5] = { 1,2,3,4,5 };
vector<int> v(a, a+5);
cout << v.back() << endl;
cout << v.front() << endl;
return 0;
}

修改操作

接口名称 接口说明
push_back() 在末尾添加一个元素,有效元素个数加1
pop_back() 删除最后一个元素,有效元素个数减1
int main()
{
vector<int> v;
for (int i = 0; i < 5; i++)
{
v.push_back(i);
}
for (auto ch : v) cout << ch << " ";
cout << endl;
return 0;
}

容量操作

接口名称 接口说明
size() 返回容器中有效元素个数

最长公共子序列-变体

给出两个长度为$n$的整数序列,求它们的最长公共子序列(LCS)的长度,保证第一个序列中所有元素都不重复。

注意:

  • 第一个序列中的所有元素均不重复。
  • 第二个序列中可能有重复元素。
  • 一个序列中的某些元素可能不在另一个序列中出现。

序列的个数$n\leq10^6$,故无法像模板题一样用二维数组用于存储状态(数组太大了超出限制了)

编译报错如下

全局变量/静态变量过大: 如果你的程序包含了非常大的全局变量或静态数组,可能会导致它们的地址超出 ARM64 的可寻址范围。

思路

因为公共子序列中值的相同的,所以数组2中公共子序列中的值放到数组1中数组下标仍然相同。所以:求公共子序列因为不用求具体的序列是多少,我们转化为求公共子序列的下标,因为下标是递增的。 所以转化为求最长上升子序列,这个序列是值下表的序列

示例代码

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

const int N=1e6+10;
int a[N],b[N],idx[N];
vector<int> aa;


int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
idx[a[i]]=i;
}
for(int i=1;i<=n;i++){
scanf("%d",&b[i]);
b[i]=idx[b[i]];
}
for (int i = 1; i <= n; i++) {
if (b[i] == 0) continue; // 如果b[i]在a中没有出现,跳过
if (!aa.size() || b[i] > aa.back()) aa.push_back(b[i]); // 如果b[i]大于aa末尾的元素,直接加到aa末尾
int id = lower_bound(aa.begin(), aa.end(), b[i]) - aa.begin(); // 找到合适的位置替换
aa[id] = b[i]; // 贪心的方法保持一个尽可能长的递增子序列
}
cout<<aa.size();
return 0;
}

lower_bound(begin , end , val , less<type>() )

从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

std::lower_bound

  • 作用: 返回第一个 大于等于 (>=) 指定值的元素的迭代器。
  • 如果值存在: 返回该值的第一个位置。
  • 如果值不存在: 返回比目标值 大的第一个元素 位置。
  • 如果所有元素都小于目标值: 返回 end() 迭代器。
  • 反向查找小于目标值的元素: std::lower_bound 返回的迭代器减一,即 std::lower_bound(vec.begin(), vec.end(), target) - 1

std::upper_bound

  • 作用: 返回第一个 大于 (>) 指定值的元素的迭代器。
  • 如果值存在: 跳过所有相同值,返回比目标值 大的第一个元素 位置。
  • 如果值不存在: 返回比目标值 大的第一个元素 位置。
  • 如果所有元素都小于等于目标值: 返回 end() 迭代器。
  • 反向查找小于等于目标值的元素: std::upper_bound 返回的迭代器减一,即 std::upper_bound(vec.begin(), vec.end(), target) - 1

20250522-动态规划

编辑距离

两个字串之间,由一个转换成另一个所需的最少编辑操作次数

$D[i][j]$ 代表着word1的前i个字符转换成word2的前j个字符所需要的最少操作步数,

当我们获得 $$D[i][j-1]$$,$$D[i-1][j]$$ 和 $$D[i-1][j-1]$$ 的值之后就可以计算出 $$D[i][j]$$ 。

  • 增:$$D[i][j-1]$$ 为 A 的前 i 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们在 A 的末尾添加了一个相同的字符,那么 $$D[i][j]$$ 最小可以为 $$D[i][j-1] + 1$$;

  • 删:$$D[i-1][j]$$ 为 A 的前 i - 1 个字符和 B 的前 j 个字符编辑距离的子问题。我们在 A 的末尾删去了一个字符(第 i 个字符),那么 $$D[i][j]$$ 最小可以为 $$D[i-1][j] + 1$$;

  • 改:$$D[i-1][j-1]$$ 为 A 前 i - 1 个字符和 B 的前 j - 1 个字符编辑距离的子问题。

    • 对于 B 的第 j 个字符,我们修改 A 的第 i 个字符使它们相同,那么 $$D[i][j]$$ 最小可以为 $$D[i-1][j-1] + 1$$。
    • 特别地,如果 A 的第 i 个字符和 B 的第 j 个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,$$D[i][j]$$ 最小可以为 $$D[i-1][j-1]$$ 。

那么我们可以写出如下的状态转移方程:

  • 若 A 和 B 的最后一个字母相同:

$$
D[i][j] = \min(D[i][j - 1] + 1, D[i - 1][j] + 1, D[i - 1][j - 1])
= 1 + \min(D[i][j - 1], D[i - 1][j], D[i - 1][j - 1])
$$

  • 若 A 和 B 的最后一个字母不同:

$$
D[i][j] = 1 + \min(D[i][j - 1], D[i - 1][j], D[i - 1][j - 1])
$$

初始化:通过前面的数组之间的关系式,我们可以知道表达式左侧i和j的值最小从1开始,不能为0,因为如果为0的话,表达式右侧数组坐标就会出现负值了。因此我们需要得到$dp[0][j]$和$dp[i][0]$

对于$dp[0][j]$来说,意味着将长度为0的字符串转换为长度为j的字符串,因此执行的操作就是不断的插入值,总共需要j步。因此 $dp[0][j] = j$,同理有 $dp[i][0] = i$

const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
for (int i = 0; i <= m; i ++ ) f[0][i] = i;// 初始化
for (int i = 0; i <= n; i ++ ) f[i][0] = i;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}

printf("%d\n", f[n][m]);

return 0;
}