线性DP问题


数字三角形

题目描述:
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

输入格式:
第一行包含整数 n n n,表示数字三角形的层数。

接下来 n n n 行,每行包含若干整数,其中第 i i i 行表示数字三角形第 i i i 层包含的整数。

输出格式:
输出一个整数,表示最大的路径数字和。

数据范围:
1 ≤ n ≤ 500 , − 10000 ≤ 1≤n≤500,−10000≤ 1n500,10000三角形中的整数 ≤ 10000 ≤10000 10000

输入样例:

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

输出样例:

30

DP 动态规划 [自上向下+二维数组]

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 510;
int a[N][N], f[N][N];

int main()
{
	int n;
	cin >> n;
	memset(a, -0x3f, sizeof a);
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= i; ++j)
		{
			cin >> a[i][j];
			f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
		}
	}
	int res = -0x3f;
	for (int i = 1; i <= n; ++i) res = max(res, f[n][i]);
	cout << res << endl;
	return 0;
}

DP 动态规划 [自上向下+一维数组]

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 510;
int a[N][N], f[N];
int main()
{
	int n;
	cin >> n;
	memset(a, -0x3f, sizeof a);
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= i; ++j)
			cin >> a[i][j];
	}
	for (int i = 1; i <= n; ++i)
	{
		for (int j = i; j >= 0; --j)
			f[j] = max(f[j - 1], f[j]) + a[i][j];
	}
	int res = -0x3f;
	for (int i = 1; i <= n; ++i) res = max(res, f[i]);
	cout << res << endl;
	return 0;
}

DP 动态规划 [自下而上+二维数组]

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 510;
int a[N][N], f[N][N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= i; ++j)
			cin >> a[i][j];
	for (int i = n; i >= 1; --i)
	{
		for (int j = 1; j <= n; ++j)
			f[i][j] = max(f[i + 1][j + 1], f[i + 1][j]) + a[i][j];
	}
	cout << f[1][1] << endl;
	return 0;
}

DP 动态规划 [自下而上+一维数组]

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 510;
int a[N][N], f[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= i; ++j)
			cin >> a[i][j];
	for (int i = n; i >= 1; --i)
	{
		for (int j = 1; j <= n; ++j)
			f[j] = max(f[j + 1], f[j]) + a[i][j];
	}
	cout << f[1] << endl;
	return 0;
}

记忆化搜索 + DFS

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 510;
int n, f[N][N], a[N][N];

int dfs(int u, int k)
{
	if (~f[u][k]) return f[u][k];
	if (k > u || u == n + 1) return 0; // 限制范围
	return f[u][k] = max(dfs(u + 1, k + 1), dfs(u + 1, k)) + a[u][k];
}
int main()
{
	cin >> n;
	memset(f, -1, sizeof f);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= i; ++j) cin >> a[i][j];
	cout << dfs(1, 1) << endl;
	return 0;
}

最长上升子序列

题目描述:
给定一个长度为 N N N 的数列,求数值 严格单调递增的子序列 的长度最长是多少。

输入格式:
第一行包含整数 N N N

第二行包含 N N N 个整数,表示完整序列。

输出格式:
输出一个整数,表示最大长度。

数据范围:
1 ≤ N ≤ 1000 , − 1 0 9 ≤ 1≤N≤1000,−10^9≤ 1N1000109数列中的数 ≤ 1 0 9 ≤10^9 109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

一维状态数组实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1010;
int f[N], a[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i)
	{
		cin >> a[i];
		f[i] = 1;
		for (int j = 1; j < i; ++j)
			if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
	}
	cout << *max_element(f + 1, f + n + 1) << endl;
	return 0;
}

扩展:最长序列输出

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1010;

int n, a[N];
int f[N];
int res, pos; // LIS最大长度  pos:最大长度是哪个下标数字提供
int pre[N];  //记录转移的前序关系

//循环+vector方式打印路径
void print(int k) {
    vector<int> path; //因为查找的关系是逆序的,需要用一个向量数组把这个逆序反过来,才能输出
    while (k) {
        path.push_back(a[k]);
        k = pre[k];
    }
    //倒序输出LIS序列
    for (int i = path.size() - 1; i >= 0; i--) printf("%d ", path[i]);
}

//递归方式打印路径
//这个递归输出逆序路径,真是太妙了,把递推与逆序的通用性展示的淋漓尽致,把别人正常用7行才能完成的代码,它只需要2行,真是太精炼了,又好写又好记。
void out(int k) {
    if (pre[k]) out(pre[k]); //因为最前面的第1号,它的前序
    printf("%d ", a[k]);
}

int main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i++) {
        f[i] = 1;
        for (int j = 1; j < i; j++)
            if (a[i] > a[j] && f[i] <= f[j]) {
                f[i] = f[j] + 1;
                pre[i] = j; // f[i]的前序是f[j]
            }

        // 更新最大值
        if (f[i] > res) {
            res = f[i]; //记录LIS最大值
            pos = i;    //记录LIS最大值时相应的数组下标i
        }
    }

    //输出LIS最大长度
    printf("%d\n", res);

    //输出前序数组查看情况
    for (int i = 1; i <= n; i++) cout << pre[i] << " ";
    puts("");

    //循环+vector方式输出LIS路径
    //从最大LIS值的下标开始,配合pre数组,不断向前以蛇形方式逆序查找前序
    print(pos);

    puts("");

    //递归方式输出LIS路径
    out(pos);

    return 0;
}

最长上升子序列 II

题目描述:
给定一个长度为 N N N 的数列,求数值 严格单调递增的子序列 的长度最长是多少。

输入格式:
第一行包含整数 N N N

第二行包含 N N N 个整数,表示完整序列。

输出格式:
输出一个整数,表示最大长度。

数据范围:
1 ≤ N ≤ 100000 1≤N≤100000 1N100000 − 1 0 9 ≤ −10^9≤ 109数列中的数 ≤ 1 0 9 ≤10^9 109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

贪心+二分优化

算法思路

核心思想:
对于同样长度的子串,希望它的末端越小越好,这样后面有更多机会拓展它,才有可能使得数列更长。

算法步骤:

扫描每个原序列中的数字:
如果 q q q 中的最后一个数字 q [ i d x ] q[idx] q[idx] 小于当前数字 a [ i ] a[i] a[i],那么就在 q q q 的最后面增加 a [ i ] a[i] a[i]
如果 a [ i ] a[i] a[i] 小于 q [ i d x ] q[idx] q[idx],在 q q q 中查找并替换第一个大于等于它元素。

模拟过程:

a3121856

开始时 q [ ] q[] q[] 为空,数字3进入序列。

arr3121856
q3

1 比 3 小,3出序列 ,1入序列。

a3121856
q1

比 1 大,2入序列。

a3121856
q1
a3121856
q12

比 2 小,在 q q q 中找到第一个大于等于1的位置,并替换掉原来的数字。

a3121856
q12

8 比 2 大,入序列。

a3121856
q128

5 比 8 小,在 q q q 中找到第一个大于等于5的数字,并替换掉原来的数字。

a3121856
q125

6 比 5 大,入序列。

a3121856
q1256

最后 q q q 的长度 idx + 1 就是最长严格递增子序列的长度。但是 q q q 所存储的序列一般不是最短序列,另类的一种全为最小值的最长严格递增的乱序序列。

例如:
7 3 4 5 1 8 3 输出 :1 4 5 8(保存了最长严格递增子序列的长度)
但是正确答案:3 4 5 8(由于 最小值 1 替代了 3)


代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int q[N], a[N], idx;

int find(int target, int range)
{
	int l = 0, r = range;
	while (l < r)
	{
		int mid = l + r >> 1;
		if (q[mid] >= target) r = mid;
		else l = mid + 1;
	}
	return l;
}
int main()
{
	int n;
	cin >> n;

	for (int i = 0; i < n; ++i) cin >> a[i];
	q[0] = a[0];
	for (int i = 1; i < n; ++i)
	{
		if (q[idx] < a[i]) q[++idx] = a[i];
		else q[find(a[i], idx)] = a[i];
	}
	cout << idx + 1 << endl;
	return 0;
}

扩展: 最长序列输出

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

const int N = 1e3 + 10;
int q[N], a[N], pos[N], idx;

void print_path(int n)
{
	vector<int> path;
	for (int i = n - 1; i >= 0 && idx >= 0; --i)
	{
		if (pos[i] != idx) continue;
		path.push_back(a[i]);
		idx--;
	}
	for (int i = path.size() - 1; i >= 0; --i) cout << path[i] << ' ';
	cout << endl;
}
int binary_search(int target, int len)
{
	int l = 0, r = len;
	while (l < r)
	{
		int mid = l + (r - l) / 2;
		if (q[mid] >= target) r = mid;
		else l = mid + 1;
	}
	return l;
}
int main()
{
	int n;
	cin >> n;
	for (int i = 0; i < n; ++i) cin >> a[i];
	q[0] = a[0];
	for (int i = 1; i < n; ++i)
	{
		if (a[i] > q[idx])
		{
			q[++idx] = a[i];
			pos[i] = idx;
		}
		else
		{
			int t = binary_search(a[i], idx);
			q[t] = a[i];
			pos[i] = t;
		}
	}
	cout << idx + 1 << endl;
	print_path(n);
	return 0;
}

最长公共子序列

题目描述:
给定两个长度分别为 N N N M M M 的字符串 A A A B B B,求既是 A A A 的子序列又是 B B B 的子序列的字符串 长度最长 是多少。

输入格式:
第一行包含两个整数 N N N M M M

第二行包含一个长度为 N N N 的字符串,表示字符串 A A A

第三行包含一个长度为 M M M 的字符串,表示字符串 B B B

字符串均由小写字母构成。

输出格式:
输出一个整数,表示最大长度。

数据范围:
1 ≤ N , M ≤ 1000 1≤N,M≤1000 1N,M1000

输入样例:

4 5
acbd
abedc

输出样例:

3

算法思路

在这里插入图片描述


代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

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

int main()
{
	int n, m;
	cin >> n >> m >> a + 1 >> b + 1;
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
		{
			if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
			else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
		}
	cout << f[n][m] << endl;
	return 0;
}

最短编辑距离

题目描述:
给定两个字符串 A A A B B B,现在要将 A A A 经过若干操作变为 B B B,可进行的操作有:

  1. 删除–将字符串 A A A 中的某个字符删除。
  2. 插入–在字符串 A A A 的某个位置插入某个字符。
  3. 替换–将字符串 A A A 中的某个字符替换为另一个字符。

现在请你求出,将 A A A 变为 B B B 至少需要进行多少次操作。

输入格式:
第一行包含整数 n n n,表示字符串 A A A 的长度。

第二行包含一个长度为 n n n 的字符串 A A A

第三行包含整数 m m m,表示字符串 B B B 的长度。

第四行包含一个长度为 m m m 的字符串 B B B

字符串中均只包含大小写字母。

输出格式:
输出一个整数,表示最少操作次数。

数据范围:
1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000

输入样例:

10 
AGTCTGACGC
11 
AGTAAGTAGGC

输出样例:

4

算法思路

在这里插入图片描述

代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

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

int main()
{
	int n, m;
	cin >> n >> a + 1;
	cin >> m >> b + 1;
	for (int i = 0; i <= n; ++i) f[i][0] = i; // 提前赋值,避免边界问题
	for (int i = 0; i <= m; ++i) f[0][i] = i;
	
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
		{
			f[i][j] = min(f[i - 1][j], 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); // 改的情况
		}
	cout << f[n][m] << endl;
	return 0;
}

编辑距离

题目描述:
给定 n n n 个长度不超过 10 10 10 的字符串以及 m m m 次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的 n n n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

输入格式:
第一行包含两个整数 n n n m m m

接下来 n n n 行,每行包含一个字符串,表示给定的字符串。

再接下来 m m m 行,每行包含一个字符串和一个整数,表示一次询问。

字符串中只包含小写字母,且长度均不超过 10 10 10

输出格式:
输出共 m m m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。

数据范围:
1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000

输入样例:

3 2
abc
acd
bcd
ab 1
acbd 2

输出样例:

1
3

代码实现

相当于对最短编辑距离的多次运用

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 15, M = 1010;
char str[M][N];
int f[N][N];
int n, m;

int edit_distance(char a[], char b[])
{
	int la = strlen(a + 1), lb = strlen(b + 1);
	for (int i = 0; i <= la; ++i) f[i][0] = i;
	for (int i = 0; i <= lb; ++i) f[0][i] = i;

	for (int i = 1; i <= la; ++i)
		for (int j = 1; j <= lb; ++j)
		{
			f[i][j] = min(f[i][j - 1], f[i - 1][j]) + 1;
			f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
		}
	return f[la][lb];
}
int main()
{
	cin >> n >> m;
	for (int i = 0; i < n; ++i) cin >> (str[i] + 1);
	while (m--)
	{
		int limit, res = 0;
		char a[N];
		cin >> a + 1 >> limit;
		for (int i = 0; i < n; ++i)
			if (edit_distance(str[i], a) <= limit) res++;
		cout << res << endl;
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值