最长上升子序列模型
最长上升子序列模型
怪盗基德的滑翔翼
假设城市中一共有
幢建筑排成一条线,每幢建筑的高度各不相同 初始时,怪盗基德可以在任何一幢建筑的顶端。
他可以选择一个方向逃跑,但是不能中途改变方向
怪盗基德只能从较高的建筑滑翔到较低的建筑
他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?
题解:最长上升子序列 : 可以利用贪心优化至
我们发现其从较高建筑滑翔至较低建筑的过程,并求其最多可以经过多少建筑,这个过程本质上和最长上升子序列问题是一样的,所以实际上我们只需要正序做一次最长上升子序列,倒序做一次最长上升子序列,然后枚举从每一个建筑出发即可
const int N = 1e2 + 10, M = 4e5 + 10;
int n;
int a[N];
int pre_f[N];
int suf_f[N];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
pre_f[i] = suf_f[i] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[i] > a[j])
pre_f[i] = max(pre_f[i], pre_f[j] + 1);
for (int i = n; i >= 1; i--)
for (int j = n; j > i; j--)
if (a[i] > a[j])
suf_f[i] = max(suf_f[i], suf_f[j] + 1);
int ans = -INF;
for (int i = 1; i <= n; ++i)
ans = max({ans, pre_f[i], suf_f[i]});
cout << ans << endl;
}
合唱队形
位同学站成一排,音乐老师要请其中的 位同学出列,使得剩下的 位同学排成合唱队形。 合唱队形是指这样的一种队形:设
位同学从左到右依次编号为 他们的身高分别为 则他们的身高满足 你的任务是,已知所有
位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
题解:最长上升子序列
题目中让我们计算最少需要多少个同学出列,我们可以将题目转化为最多有多少位同学可以拍成合唱队形,那么多余的人数就是最少需要出列的同学人数
我们发现最高的同学i左侧是最长上升子序列,右侧是最长下降子序列,那么我们只需要枚举将每一位同学作为合唱队形中最高的同学即可,注意最后两个长度加起来时需要减去1,因为根据容斥,中间最高的那名同学多算了1次
const int N = 1e3 + 10, M = 4e5 + 10;
int n;
int a[N];
int pre_f[N];
int suf_f[N];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
pre_f[i] = suf_f[i] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[i] > a[j])
pre_f[i] = max(pre_f[i], pre_f[j] + 1);
for (int i = n; i >= 1; i--)
for (int j = n; j > i; j--)
if (a[i] > a[j])
suf_f[i] = max(suf_f[i], suf_f[j] + 1);
int ans = -INF;
for (int i = 1; i <= n; ++i)
ans = max({ans, pre_f[i] + suf_f[i] - 1});
cout << n - ans << endl;
}
最大上升子序列和
给你一个子序列,让你求出该序列中的最大上升子序列和
题解:线性
状态表示:
代表以第 个数结尾的最长上升子序列和 状态属性:
状态计算:
状态初始:
答案呈现:
const int N = 1e3 + 10, M = 4e5 + 10;
int n;
int a[N];
int f[N];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
f[i] = a[i];
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[j] < a[i])
f[i] = max(f[i], f[j] + a[i]);
int ans = -INF;
for (int i = 1; i <= n; ++i)
ans = max(ans, f[i]);
cout << ans << endl;
}
拦截导弹
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于
的正整数,导弹数不超过 ),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统
题解:贪心 + : / 定理
显然第一个问题是一个非常明显的最长不上升子序列问题,做法两种:一种是
,另一种是贪心: ,我们这里选择的是贪心的方法,不再赘述 我们来想一想第二个问题:
我们将第二个问题转化为:一个序列最少需要用多少个不上升子序列能够将其覆盖掉
也就是说我们需要将每个数分配到一个不上升子序列中,我们考虑贪心解决,类似最长不上升子序列的贪心方法:
- 我们利用数组
维护所有不上升子序列的结尾数字 - 对于每一个需要分配的数字
,我们贪心的将其分配进入所有结尾数字中第一个比 大的不上升序列中 - 如果数字
比每一个结尾数字都要大的话,说明 需要自己作为一个不上升子序列,即新建一个不上升子序列 我们发现数组
中的结尾数字单调递增,所以每次分配的位置我们可以利用二分找到,那么最终复杂度为 我们惊奇的发现其贪心的过程不就是求最长子序列长度的贪心过程嘛,实际上这里存在一个定理:
定理:一个序列最少用多少个不上升子序列将其覆盖掉的数量等于该序列最长上升子序列的长度
const int N = 1e3 + 10, M = 4e5 + 10;
int n;
int a[N];
int f[N];
multiset<int> st;
void solve()
{
int x;
while (cin >> x)
a[++n] = x;
int len = 0;
for (int i = 1; i <= n; ++i)
{
if (len == 0 || (len > 0 && a[i] <= f[len]))
f[++len] = a[i];
else if (len > 0 && a[i] > f[len])
{
int p = upper_bound(f + 1, f + len + 1, a[i], greater<int>()) - f;
f[p] = a[i];
}
}
int ans2 = 0;
for (int i = 1; i <= n; ++i)
{
if (st.empty() || (st.size() && *(prev(st.end())) < a[i]))
{
ans2++;
st.insert(a[i]);
}
else if (st.size() && *(prev(st.end())) >= a[i])
{
st.erase(st.lower_bound(a[i]));
st.insert(a[i]);
}
}
cout << len << endl;
cout << ans2 << endl;
}
导弹防御系统
为了对抗附近恶意国家的威胁,
国更新了他们的导弹防御系统。 一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为
和高度为 的两发导弹,那么接下来该系统就只能拦截高度大于 的导弹 给定即将袭来的一系列
个导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落
题解: 求最优解(迭代加深 / 全局最优解) + 贪心
我们将题目转化为:一个序列最少需要用多少个上升子序列和下降子序列能够将其覆盖掉
如果说只用一种序列,我们完全可以用拦截导弹的贪心方法解决,但是这里既可以用上升子序列,又可以用下降序列,并且数据范围比较小,所以我们完全可以使用
爆搜 + 剪枝解决
- 首先我们需要明确搜索的顺序,怎样才能枚举完所有的方案?
对于每一个数,我们枚举该数放在上升序列还是放在下降序列中
如果该数放在上升序列,那么我们枚举该数放在哪个上升序列中
如果该数放在下降序列,那么我们枚举该数放在哪个下降序列中
对于上述的搜索顺序,一定能够枚举完所有的合法方案
但是复杂度太高了,我们需要对其剪枝,我们发现我们维护一个所有上升序列结尾数字数组
和一个所有下降序列结尾数字数组 ,这样的话我们借用拦截导弹的贪心思想:
如果我们将一个数放在某个上升序列的末尾,那么根据贪心的思想,这个数一定是放在第一个末尾数字比这个数小的上升序列后面,如果我们找不到任何一个上升序列放该数字,说明该数字单独作为一个新的上升序列
如果我们将一个数放在某个下降序列的末尾,那么根据贪心的思想,这个数一定是放在第一个末尾数字比这个数大的下降序列后面,如果我们找不到任何一个下降序列放该数字,说明该数字单独作为一个新的下降序列
那么此时我们发现我们维护的
数组一定是单调递减的, 数组一定是单调递增的,所以我们在搜索时枚举该数放在哪个上升或者下降序列时,只要枚举到 数组中第一个比这个数小的序列末尾或者枚举到 数组中第一个比这个数大的序列末尾即可,这是一个很不错的剪枝 但是我们还需要考虑一个问题,如何在
爆搜的过程中得到最小值: 有两种方法:
- 迭代加深:一般用于平均答案比较小的情况下,比如说本题,在本题中我们迭代的深度
代表现在上升序列和下降序列的个数之和,所以说如果我们在某一次迭代过程中上升序列和下降序列之和超过了 ,我们可以直接退出迭代,进行下一次迭代,直到迭代成功 - 全局最优解:在枚举的所有可行方案中找到全局最优解,但是需要注意剪枝:如果现在的上升序列和下降序列之和已经大于等于了当前最优解,直接退出即可,说明当前解不可能产生全局最优解
//方法一:迭代加深
const int N = 55, M = 4e5 + 10;
int n;
int a[N];
int up[N];
int down[N];
bool dfs(int depth, int u, int num_up, int num_down)
{
if (num_up + num_down > depth)
return false;
if (u == n + 1)
return true;
// 枚举放入单调上升序列中
bool flag = false;
for (int i = 1; i <= num_up; ++i) // up数组单调递减
{
if (up[i] < a[u])
{
int t = up[i];
up[i] = a[u];
if (dfs(depth, u + 1, num_up, num_down))
return true;
up[i] = t; //还原现场
flag = true;
break; //贪心剪枝,直接可以退出
}
}
if (!flag) //如果任何一个上升序列都放不进
{
up[num_up + 1] = a[u];
if (dfs(depth, u + 1, num_up + 1, num_down))
return true;
down[num_down + 1] = 0;
}
// 枚举放入单调下降序列中
flag = false;
for (int i = 1; i <= num_down; ++i) // down数组单调递增
{
if (down[i] > a[u])
{
int t = down[i];
down[i] = a[u];
if (dfs(depth, u + 1, num_up, num_down))
return true;
down[i] = t; //还原现场
flag = true;
break; //贪心剪枝
}
}
if (!flag) //如果任何一个下降序列都放不进
{
down[num_down + 1] = a[u];
if (dfs(depth, u + 1, num_up, num_down + 1))
return true;
down[num_down + 1] = 0;
}
return false;
}
void solve()
{
while (cin >> n, n)
{
for (int i = 1; i <= n; ++i)
cin >> a[i];
int depth = 0;
while (!dfs(depth, 1, 0, 0)) //迭代加深,直到迭代成功
depth++;
cout << depth << endl;
}
}
//方法二:全局最优解
const int N = 55, M = 4e5 + 10;
int n;
int a[N];
int up[N];
int down[N];
int ans;
void dfs(int u, int num_up, int num_down)
{
if (num_up + num_down >= ans) //如果大于当前最优解,直接退出
return;
if (u == n + 1)
{
ans = min(ans, num_up + num_down);
return;
}
// 枚举放入单调上升序列中
bool flag = false;
for (int i = 1; i <= num_up; ++i) // up数组单调递减
{
if (up[i] < a[u])
{
int t = up[i];
up[i] = a[u];
dfs(u + 1, num_up, num_down);
up[i] = t;
flag = true;
break;
}
}
if (!flag)
{
up[num_up + 1] = a[u];
dfs(u + 1, num_up + 1, num_down);
up[num_up + 1] = 0;
}
// 枚举放入单调下降序列中
flag = false;
for (int i = 1; i <= num_down; ++i) // down数组单调递增
{
if (down[i] > a[u])
{
int t = down[i];
down[i] = a[u];
dfs(u + 1, num_up, num_down);
down[i] = t;
flag = true;
break;
}
}
if (!flag)
{
down[num_down + 1] = a[u];
dfs(u + 1, num_up, num_down + 1);
down[num_down + 1] = 0;
}
}
void solve()
{
while (cin >> n, n)
{
for (int i = 1; i <= n; ++i)
cin >> a[i];
ans = 100;
dfs(1, 0, 0);
cout << ans << endl;
}
}
最长公共上升子序列
给定长度都为
的序列a,b,求其最长公共上升子序列的长度
题解:线性 :
- 状态表示:
代表在 和 中的,且以 结尾的公共上升子序列的最大长度
状态属性:
状态计算:按照公共上升子序列中
是否存在分为两个集合:
不在公共上升子序列中: - 如果
在公共上升子序列中,必须满足 ,那么这一集合可以按照 接在哪个上升序列的末尾后面继续划分:
不接在任何一个上升序列后面: 接在 后面 : 接在 后面 : - ......
接在 后面 :
状态初始:
答案呈现:
时间复杂度:
状态优化:我们发现只有
时,我们才会讨论 接在哪个上升序列的末尾后面,假设 接在末尾为 的子序列后面,则 ,即 ,所以我们不妨在寻找 的同时顺便维护 中的 的最大值,优化后时间复杂度为
const int N = 3e3 + 10, M = 4e5 + 10;
int n;
int f[N][N]; // f[i][j]代表所有[1,i]和[1,j]中的,且以b[j]结尾的公共上升子序列的最大长度
int a[N], b[N];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
cin >> b[i];
for (int i = 1; i <= n; ++i)
{
int mx = 1;
for (int j = 1; j <= n; ++j)
{
f[i][j] = f[i - 1][j];
if (a[i] == b[j])
f[i][j] = max(f[i][j], mx);
else if (a[i] > b[j])
mx = max(mx, f[i - 1][j] + 1);
}
}
int ans = -INF;
for (int i = 1; i <= n; ++i)
ans = max(ans, f[n][i]);
cout << ans << endl;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】