最大连续子序列
数据num(N)
令d(i)表示num(i)最为末尾的连续序列的最大和。
转移方程
d(i)=max(d(i-1)+num(i),num(i))
有一点需要注意,递归数组的最后未必是以num(N)最大最大
核心代码
dp[0] = num[0];
for (int i = 1; i <N; i++) {
if (dp[i - 1] + num[i] > num[i]) {
dp[i] = dp[i - 1] + num[i];
//indx[i] = indx[i - 1];
}
else {
dp[i] = num[i]; //indx[i] = i;
}
}
int flag=0;
for(int i=1;i<=n;i++)
if(d[flag]<d[i]) flag=i;
最长不下降序列(LIS)
在一连串数字中找出最长的非递减串,不需要连续
令 d(i)表示以num(i)结尾的最长非下降序列
d(i)=max(i,dp(j)+1)(j=1,2,3.....i-1&&num(j)<num(i))
同样最后的数据也未必是最大的
memset(dp, 0, sizeof(dp));
int ans = -1;
for (int i = 0; i < num; i++) {
for (int j = 0; j < i; j++) {
if (b[i] >= b[j] && dp[i] > dp[j] + 1)
dp[i] = dp[j] + 1;
}
ans = max(dp[i], ans);
}
最长公共子序列
(1)只能一一对应,一个字符匹配过后不能在匹配第二次
d(i,j)=max( d(i-1,j),d(i,j-1),d(i-1,j-1)+(s1[i]==s2[j]) )
(2)同一个字符可以连续匹配多次,注意是连续,不能间隔
d(i,j)=max( d(i-1,j),d(i,j-1))+(s1[i]==s2[j])
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
d2[i][j] = max(max(d2[i - 1][j], d2[i][j - 1]),d[i-1][j-1]+(a[i] == b[j]))
d2[i][j] = max(d2[i - 1][j], d2[i][j - 1]) + (a[i] == b[j]);
}
最长回文子串
即给定一个字符串s,判断所谓s中最长的回文子序列
这里容易犯一个错误:把这个问题转化为最长公共子序列问题,将S逆序T,然后匹配出一个(LCS),但是如果匹配的字符串序列不是连续的,结果一定出错,就算是连续的也不可以,因为即使是回文子串也未必能吻合
最简单的 S=" abcdba", T=“abdcab”
我们对长度进行匹配
for (int i = 0; i < len; i++) {
d[i][i] = 1; //初始化为1
if (i + 1 < len&&s[i] == s[i + 1]) d[i][i + 1]=1,ans=2; //提前判断长度为2的。
}
for (int L = 3; L <= len; L++) {
for (int i = 0; i + L - 1 < len; i++) {
int j = i + L - 1;
if (d[i + 1][j - 1] && s[i] == s[j]) d[i][j] = 1, ans = L;
}
}
DAG最短路,最长路
看过了好多文章,截止到现在最难理解的就是构图是如何构图的,使用那样的转移方程为什么不会出现闭环,出现死循环,还有最重要的一点就是关于输出时的字典序问题,如何最小,如何最大,都是问题啊啊啊啊啊,难理解,先记在这里,将来还是会忘的?
解惑: 我们首先来看两个例子 参自刘汝佳《算法入门经典》
1.嵌套矩形问题。有n个矩形,每个矩形可以用两个整数a、b描述,表示它的长和宽。矩 形X(a,b)可以嵌套在矩形Y(c, d)中,当且仅当a<c,b<d,或者b<c,a<d(相当于把矩 形X旋转90°)。例如,(1, 5)可以嵌套在(6, 2)内,但不能嵌套在(3, 4)内。你的任务是选出尽 量多的矩形排成一行,使得除了最后一个之外,每一个矩形都可以嵌套在下一个矩形内。如 果有多解,矩形编号的字典序应尽量小。
2.硬币问题。有n种硬币,面值分别为V1, V2, …, Vn,每种都有无限多。给定非负整数S, 可以选用多少个硬币,使得面值之和恰好为S?输出硬币数目的最小值和最大值。 1≤n≤100,0≤S≤10000,1≤Vi≤S。
(1)问:如何构图?
答:我们把每一个矩形视作一个顶点,对于A,B两个矩形,如果B可以完全嵌套进A,即B完全可以把A包进去,就让建立一个从A至B点的边(这里我们设为小的指向大的矩形,当然也可以大的矩形指向小的矩形),权值为1;
(2)为什么不会出现闭环?
答:何为环?图中存在一点,或者几点从自身出发,总有一条路径可以使它再次回到自身上来。既然如此,我们不妨设从X出发通过一条路径回到Y再到X当中来,那么Y小于X,但从我们的定义上来看,X所指向的每一个顶点,必然会比X大,矛盾!!那有没有可能使自身成环呢,当然有可能,无可能,题目中所给是完全嵌套,不存在相等的情况下,因此,我们所构建的图中一定不存在环。那也就不存在死循环了。
这里注意一下,我们所构建的图的起点并不唯一,终点也不唯一。
贴一下刘大神的代码
入口
**转移方程 :d(i)=max(d(j)+1,d(i)) (i,j)属于E**
for(int i=1;i<=n;i++)
solve=dp(i);
int dp(int i)
{ int& ans = d[i];
if(ans > 0) return ans; 0表示没有被访问,因为一个矩形嵌套算上自己
ans = 1;
for(int j = 1; j <= n; j++)
if(G[i][j]) ans = max(ans, dp(j)+1);
return ans;
}
void print_ans(int i) {2
printf("%d ", i);
for(int j = 1; j <= n; j++)
if(G[i][j] && d[i] == d[j]+1){
print_ans(j); break;
}
}
也可以逆向
d(i)=max(d(j)+1,d(i)) (j,i)属于E
int dp(int i)
{ int& ans = d[i];
if(ans > 0) return ans;
ans = 1;
for(int j = 1; j <= n; j++)
if(G[j][i]) ans = max(ans, dp(j)+1);
return ans;
}
//
(3)如何保证确定的路径字典序最小呢?
字典序最小是一种最常见的求唯一解的方法
最笨的方法就是把每一个路径都记录下来,然后比较。
还有一种更好的方法就是直接判断d(i),对于相同的d(i)我们选取最小的i即可,如果是这样的话就必须选择第一种构图方法,我只是再代码里附上另一种方法。
(4)如何打印路径
按照求最长路的方法打印即可。
拓扑方法有空再写
再看第二题:
(1)问:如何构图?
我们选取0–S作为顶点,硬币的额度为边,例如我们有1元,2元硬币,这里的起点和终点是唯一的,起点是S,终点是0;
那么0-1,1-2,2-3,3-4…(S-1)-S存在权值为1的边
0-2,1-3,2-4.。。。。(S-2)–S存在权值为2的边,以此类推。
边权同样是1;
(2)为什么不会出现闭环?
我们同样从X出发通过一条路径回到Y再到X当中来,那么Y一定大于X,但从我们的定义上来看,X所指向的每一个顶点,必然会比X小,矛盾!!
因此不存在环。
最长路算法
初始化不要为0,因为0也可使是结果,这与嵌套矩形不同。
int dp(int S){
int& ans = d[S];
if(ans != -1) return ans;
ans = -(1<<30);
for(int i = 1; i <= n; i++)
if(S >= V[i])
ans = max(ans, dp(S-V[i])+1);
return ans;
}
后一种使用辅助数组判断是否被访问,前一种是d[i]=-1表示没有被访问
int dp(int S){
if(vis[S]) return d[S];
vis[S] = 1;
int& ans = d[S];
ans = -(1<<30);
for(int i = 1; i <= n; i++)
if(S >= V[i])
ans = max(ans, dp(S-V[i])+1); return ans; }
也可以使用递推,这个不需要先拓扑排序,因为本身就有一定的顺序
minv[0] = maxv[0] = 0;
for(int i = 1; i <= S; i++){
minv[i] = INF; maxv[i] = -INF; }
for(int i = 1; i <= S; i++)
for(int j = 1; j <= n; j++)
if(i >= V[j]){
minv[i] = min(minv[i], minv[i-V[j]] + 1);
maxv[i] = max(maxv[i], maxv[i-V[j]] + 1);
}
其实也可以看作是完全背包一维数组的写法,后面会提到
printf("%d %d\n", minv[S], maxv[S])
字典序输出,矩形嵌套。
void print_ans(int* d, int S){
for(int i = 1; i <= n; i++)
if(S>=V[i] && d[S]==d[S-V[i]]+1){
printf("%d ", i);
print_ans(d, S-V[i]);
break;
}
}
还有一种方法就是刘大神书中写的,这里先贴上,有空在看
很多用户喜欢另外一种打印路径的方法:递推时直接用min_coin[S]记录满足min[S] ==>min[S-V[i]]+1的最小的 i,则打印路径时可以省去print_ans函数中的循环,并可以方便 地把递归改成迭代(原来的也可以改成迭代,但不那么自然)。具体来说,需要把递推过程 改成以下形式:
for(int i = 1; i <= S; i++)
for(int j = 1; j <= n; j++)
if(i >= V[j]){
if(min[i] > min[i-V[j]] + 1){
min[i] = min[i-V[j]] + 1;
min_coin[i] = j; }
if(max[i] < max[i-V[j]] + 1){
max[i] = max[i-V[j]] + 1;
max_coin[i] = j;
}
}
注意,判断中用的是“>”和“<”,而不是“>=”和“<=”,原因在于“字典序最小解”要求 当min/max值相同时取最小的i值。反过来,如果j是从大到小枚举的,就需要把“>”和“<”改 成“>=”和“<=”才能求出字典序最小解。
在求出min_coin和max_coin之后,只需调用print_ans(min_coin, S)和print_ans(max_coin, S) 即可。
用时间换空间,有空在看
void print_ans(int* d, int S){
while(S){
printf("%d ", d[S]);
S -= V[d[S]];
}
}
接下来就是重头戏
背包问题
0-1背包
给定一批物体N,每个物体各有重量weight[i]和价值value[i],求出在满足背包重量的情况下,每个物品只能装一次,那么最大价值是多少
d(i,j)=max(d(i-1,j),d(i-1,j-weight[i])+value[i]);
二维数组
memset(d,0,szieof(d))
for(int i=1;i<=n;i++)
for (int j = v; j >= weight[i]; j--) {
d[i][j]=max(d[i-1][j],d[i][j-weight[i]]+value[i]);
int i=n,j=v;
while(i>=0){
if(d[i][j]==d[i-1][j-weight[i]]+value[i]) //装入i个物品{
cout<<i<<' ';
j-=weight[i];
}
i--;
}
优化 一维滚动数组
for(int i=1;i<=n;i++)
for (int j = v; j >= weight[i]; j--) {
if (d[j]<=d[j - weight[i]] + value[i]) {
d[j] = d[j - weight[i]] + value[i];
choose[i][j] = 1;
}
else choose[i][j] = 0;
}
while (i > 0) { //这里采用了辅助数组来输出
if (choose[i][j] == 1) {
cout << weight[i] << ' ';
j -= weight[i];
}
i--;
}
关于滚动数组的一些问题可以查看大佬
完全背包
数量上无限个
d(i,j)=max(d(i-1,j),d(i,j-weight[i])+value[i]);
memset(d,0,sizeof(d));
for (int i = 1; i <= n; i++) { //完全背包
for (int j = weight[i], j <= v; j++)
d[i][j] = max(d[i - 1][j], d[i][j - weight[i]] + value[i]);
}
一维数组
for (int i = 1; i <= n; i++) { //完全背包
for (int j = weight[i], j <= v; j++)
d[j] = max([j], d[j - weight[i]] + value[i]);
}
建议使用辅助数组进行输出方案,同上