线性DP算法分析:
具有线性“阶段”划分的动态规划算法。
无论线性DP的状态表示上是一维的还是多维的,都是线性上的递推。DP的阶段远着各个维度线性增长,从一个或多个边界点开始(意思是可以有多个起点),有方向地向整个状态空间转移。拓展。最后每个状态都保留了以自身为“目标”的子问题的最优解。
线性DP的主要特点:
- 推导是按照问题规模
从小到大依次推过去的,较大规模的问题的解依赖较小规模的问题的解。
- 这里问题规模为
的含义是考虑前
个元素
时问题的解。
LIS问题:
例题一: 最长上升子序列
方法一:
int main()
{
int ans = 0;
for(int i=1; i <= n; i++)
{
f[i] = 1;
for(int j=1; j <= i-1; j++)
{
if(a[i]>a[j]) dp[i] = max(dp[i],dp[j]+1);
}
ans = max(ans,dp[i]);
}
cout << ans;
}
不做过多解释,类似背包,就是节点当作权值的累加,不懂可以参考上一篇博客背包题目之——甜甜的背包算法糖们~~~https://blog.youkuaiyun.com/wangluoyi1/article/details/145291075?spm=1001.2014.3001.5501https://blog.youkuaiyun.com/wangluoyi1/article/details/145291075?spm=1001.2014.3001.5501https://blog.youkuaiyun.com/wangluoyi1/article/details/145291075?spm=1001.2014.3001.5501
方法二:贪心+二分 :
统摄一下:
考虑一个数组,每次把比数组最大的数字大的数放进去,把数组中比自己大一点的替换掉,就能实现。
解释一下:
因为
表示长度为
的 LIS 结尾元素的最小值。对于一个上升子序列,如果其结尾元素越小,是不是越利于在后面接上其他的元素,也就可能变得越长。
具体方法:
只需要去维护
数组就可以,每次用
去比较如果
就直接放进去。
如果发现
在
中有比自己大的,就循环找到比自己大的,然后替换然后你就 TLE 了 没错,你会发现时间复杂度是
相当于理论推了半天推了个几毛。那咋办?
最聪慧的一集(这才是重点):
如果按照之前的维护,会发现
是单调不降的,所以我们二分
数组,找到第一个大于等于
的元素。二分一次的时间是
,所以总的时间复杂度为
。
#define L(i, j, k) for(int i=j; i <= k; i++)
int Find()
{
int len = 0;
dp[++len] = a[1];
L(i, 2, n)
if(dp[len] <= a[i]) dp[++len] = a[i];
else{
int k = lower_bound(dp+1,dp+1+len,a[i])-dp;
dp[k] = a[i];
}
return len;
}
例题二:P1091 [NOIP2004 提高组] 合唱队形
正反跑一遍最长路,因为最后一个同学会被算两遍,因此最后要 .
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+100;
int n;
int dp1[N], dp2[N], num[N];
auto work=[]()
{
cin >> n;
L(i, 1, n)
cin >> num[i];
L(i, 1, n)
{
dp1[i] = 1;
L(j, 1, i-1)
if(num[i] > num[j])
dp1[i] = max(dp1[j]+1,dp1[i]);
}
R(i, n, 1)
{
dp2[i] = 1;
R(j, n, i+1)
if(num[i] > num[j])
dp2[i] = max(dp2[j]+1,dp2[i]);
}
int maxx = -1;
L(i, 1, n)
maxx = max(maxx,dp1[i]+dp2[i]-1);
cout << n-maxx;
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
例题三:P1020 [NOIP1999 提高组] 导弹拦截
一句废话:最懂得一集,也是收获最多的一集。
具体问题具体分析:
因为要拦截的导弹是不下降的,所以最多能拦截的导弹就是最长不下降子序列。
那问题来了:怎么知道最少配备多少拦截系统。
你需要:Dilworth 定理
- 定义:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真。
- 链和反链定义:设
是偏序集的一个子集,如果
中的元素相互可比,那么称
是链。如果
中的元素相互不可比,则称
是反链。
下面解释什么是偏序集,标红的是重点概念:
- 偏序集:偏序集是集合
上由偏序关系
构成的,我们记为
。
- 偏序关系:对于二元关系
,如果
是自反的、反对称的,传递的,那么
称为偏序关系。
- 自反性:
- 反对称性:
,若
且
则
。(这个明白了,后面的DAG的应用就没问题了,就类似有向无环,但是如果是偏序而非全序就没有这一说)
- 传递性:
,若
且
则
。
- 可比的概念:对于
中的元素
。如果
或
,则称
可比,反之则不可比。
那么应用到这道题上面,我们要求的是最少划分链数,那么根据 Dilworth定理,最小划分链数目就等于最大反链中元素数目,因此在该有限偏序集中原本序列最长不下降是具有可比性的,怎么转化为不可比的呢?最长不下降的偏序关系的反链那就是最长下降子序列。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+100;
int n, res1 = 0, res2 = 0;
int dp[N], a[N];
auto work=[]()
{
while(cin >> a[++n])
dp[n] = 1;
R(i, n, 1)
R(j, n, i+1)
if(a[i] >= a[j])
{
dp[i] = max(dp[j]+1,dp[i]);
res1 = max(dp[i],res1);
}
L(i, 1, n)
{
dp[i] = 1;
L(j, 1, i-1)
if(a[i] > a[j])
{
dp[i] = max(dp[j]+1,dp[i]);
res2 = max(dp[i],res2);
}
}
cout << res1 << endl << res2;
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
例题四:P2196 [NOIP1996 提高组] 挖地雷
此题肥肠经典的带权上升子序列啊啊啊,那我问你那我问你,你怎么输出路径怎么输出路径???(疑似常熟阿诺)
回归正题
一道拓展:最长上升子序列的路径输出模版题
先来梳理一下,既然我们要输出路径,在每次状态转移的时候给他标记,然后塞到一个数组里面那不就完了吗。怎么标记,就是要把你的
给他拆开啊。那我问你,怎么存储你的路径,那就再开一个数组专门来记录状态转移那不就完了吗,具体不懂得可以看我给的模版,有注释可以食用。
mode Code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
const int N = 1e3+10;
int dp[N], a[N], g[N]; // g[]用来记录状态转移
int n;
signed main()
{
fastio;
cin >> n;
L(i, 1, n) cin >> a[i];
int res = 0; // 记录答案
L(i, 1, n)
{
dp[i] = 1;
L(j, 1, i-1)
if(dp[i] < dp[j]+1)
{
dp[i] = dp[j]+1;
g[i] = j;// g[i]=j 表示 i 状态是由 j 状态转移过来的
res = max(res,dp[i]);
}
}
cout << res << endl;
// 下一步 :找到最大值
vector<int> path;
int pos = -1, maxx = -1;
L(i, 1, n)
if(dp[i] > maxx)
{
maxx = dp[i];
pos = i;
}
while(pos)
{
path.push_back(pos);
pos = g[pos];
}
//因为我们是顺推,类比 DAG 存储,打印必须从后往前
reverse(path.begin(),path.end());
for(auto p: path) cout << p << " ";
return 0;
}
这才是题解:(看看自己一年前的码风还是颇有感慨)
#include<bits/stdc++.h>
using namespace std;
int n;
int num[20];
int mp[20][20]={0};
int dp[20]={0};
int path[20];
int main(){
ios::sync_with_stdio(false);
cin >> n;
for(int i=0; i < n; i++)
{
cin >> num[i];
dp[i] = num[i];
}
for(int i=0; i < n; i++)
{
for(int j=i+1; j < n; j++)
{
cin >> mp[i][j];
}
}
memset(path,-1,sizeof(path));
for(int i=0; i < n; i++)
{
for(int j=0; j < n; j++)
{
if(mp[j][i] == 1&&dp[j]+num[i] > dp[i])
{
dp[i] = dp[j]+num[i];
path[i] = j;
}
}
}
int ans,max=-1,s;
for(int i=0; i < n; i++)
{
ans = dp[i];
if(ans > max)
{
max = dp[i];
s = i;
}
}
vector<int> arr;
while(s != -1)
{
arr.push_back(s);
s = path[s];
}
for(int i=arr.size()-1; i >= 0; i--)
{
cout << arr[i]+1 << ' ';
}
cout << endl;
cout << max << endl;
return 0;
}
你觉得这就结束了吗,哼哼
例题五(easy shift):P2782 友好城市
题目要求不能交叉,有个nb的方法,把一遍先从小到大排个序,然后跑最长上升子序列。
秒了!!!唯独不同的是这道题我选择用 的,刚好可以练习之前的推理。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+10;
int n;
int dp[N];
struct node{
int a, b;
bool operator < (const node x)const
{
return b < x.b;
}
}A[N];
int Find()
{
int len = 0;
dp[++len] = A[1].a;
L(i, 2, n)
{
if(A[i].a >= dp[len]) dp[++len] = A[i].a;
else{
int k = lower_bound(dp+1,dp+1+len,A[i].a)-dp;
dp[k] = A[i].a;
}
}
return len;
}
auto work=[]()
{
cin >> n;
L(i, 1, n) cin >> A[i].a >> A[i].b;
sort(A+1,A+1+n);
int t = Find();
cout << t;
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
例题六(a little bit hard):飞翔
Description
鹰最骄傲的就是翱翔,但是鹰们互相都很嫉妒别的鹰比自己飞的快,更嫉妒其他的鹰比自己飞行的有技巧。于是,他们决定举办一场比赛,比赛的地方将在一个迷宫之中。
这些鹰的起始点被设在一个N*M矩阵的左下角map[1,1]的左下角。终点被设定在矩阵的右上角map[N,M]的右上角,有些map[i,j]是可以从中间穿越的。每一个方格的边长都是100米。如图所示: 没有障碍,也没有死路。这样设计主要是为了高速飞行的鹰们不要发现死路来不及调整而发生意外。潘帕斯雄鹰冒着减RP的危险从比赛承办方戒备森严的基地中偷 来了施工的地图。但是问题也随之而来,他必须在比赛开始之前把地图的每一条路都搞清楚,从中找到一条到达终点最近的路。(哈哈,笨鸟不先飞也要拿冠军)但 是此鹰是前无古鹰,后无来鹰的吃菜长大的鹰--菜鸟。他自己没有办法得出最短的路径,于是紧急之下找到了学OI的你,希望找到你的帮助。
Input Format
首行为n,m(0<n,m<=1000000),第2行为k(0<k<=1000)表示有多少个特殊的边。以下k行为两个数,i,j表示map[i,j]是可以直接穿越的。
Output Format
仅一行,1,1-->n,m的最短路径的长度,四舍五入保留到整数即可
*************************************************华丽丽的题&解分割线************************************
- 首先路径要最短,不能走回头路,所以必须是最长上升子序列,而不是最长不下降子序列,大于和大于等于的关系要搞明白,这个题就是注意细节,练习熟练度。
- 由于没有评测系统,这题只提供关键代码不给出完整代码,大家思考后可以比对。
sort(mp+1,mp+1+k);
L(i, 1, k)
{
dp[i] = 1;
L(j, 1, i-1)
if(mp[i].y > mp[j].y)
{
dp[i] = max(dp[i],dp[j]+1);
res = max(dp[i],res);
}
}
double ans = 100*sqrt(2)*res;
ans += (n+m)*100-200*res;
printf("%.0lf",ans);
其实不用看就和上一道题一毛一样。。。
例题七: P1481 魔族密码
唯一的知识点就是 函数的用法,要保证每次新的单词前面的部分能和上一个单词一毛一样才能状态转移。just so so......,具体用法可以看代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+10;
int n, res = 0;
string s[N];
int dp[N], len[N];
auto work=[]()
{
cin >> n;
L(i, 1, n)
{
cin >> s[i];
len[i] = s[i].size();
}
L(i, 1, n)
{
dp[i] = 1;
L(j, 1, i-1)
if(s[i].substr(0,s[j].size()) == s[j])
{
dp[i] = max(dp[i],dp[j]+1);
res = max(res,dp[i]);
}
}
cout << res;
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
LCS问题:
例题一(one last dance):P1439 【模板】最长公共子序列
先来看洛谷里皎月半洒花大佬对 dp 状态转移的解释,个人认为妙不可言。
下一状态最优值=最优比较函数(已经记录的最优值,可以由先前状态得出的最优值)
——即动态规划具有 判断性继承思想
解析:我们可以用
来表示第一个串的前
位,第二个串的前
位的
的长度,那么我们是很容易想到状态转移方程的:
如果当前的
和
相同(即是有新的公共元素) 那么
如果不相同,即无法更新公共元素,考虑继承:
那么代码:
#include<iostream>
using namespace std;
int dp[1001][1001],a1[2001],a2[2001],n,m;
int main()
{
//dp[i][j]表示两个串从头开始,直到第一个串的第i位
//和第二个串的第j位最多有多少个公共子元素
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d",&a1[i]);
for(int i=1;i<=m;i++)scanf("%d",&a2[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a1[i]==a2[j])
dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
//因为更新,所以++;
}
cout<<dp[n][m];
}
其实精髓到这里我相信你已经都学到了,其实我们每个人都是普通人,只是有人愿意发光,而有人愿意成为光
即使 DP 有千千万万个类型,但就让我们一起在这个寒假把它彻底解决吧,相信终有一天我们也能成为光!