咳咳,有一部分是根据CillyB总结的
1.最大子序和
最大子序列和一定是连续的,如果不连续,就没有意义了,因为我们只需要扫描一遍数组,输出其中所有正整数即可,他们的和一定是最大
的。
很容易理解时间界O(N) 是正确的,但是要是弄明白为什么正确就比较费力了。其实这个是算法二的一个改进。分析的时候也是i代表当前序列的起点,j代表当前序列的终点。
如果我们不需要知道最佳子序列的位置,那么i就可以优化掉。
重点的一个思想是:如果a[i]是负数那么它不可能代表最有序列的起点,因为任何包含a[i]的作为起点的子序列都可以通过用a[i+1]作为起点来改进。类似的有,任何的负的子序列
不可能是最优子序列的前缀。例如说,循环中我们检测到从a[i]到a[j]的子序列是负数,那么我们就可以推进i。关键的结论是我们不仅可以把i推进到i+1,而且我们实际可以把它一直
推进到j+1。
举例来说,令p是i+1到j之间的任何一个下标,由于前面假设了a[i]+…+a[j]是负数,则开始于下标p的任意子序列都不会大于在下标i并且包含从a[i]到a[p-1]的子序列对应的子序列(
j是使得从下标i开始成为负数的第一个下标)。因此,把i推进到j+1是安全的,不会错过最优解。注意的是:虽然,如果有以a[j]结尾的某序列和是负数就表明了这个序列中的任何一个数
不可能是与a[j]后面的数形成的最大子序列的开头,但是并不表明a[j]前面的某个序列就不是最大序列,也就是说不能确定最大子序列在a[j]前还是a[j]后,即最大子序列位置不能求出。
但是能确保maxSum的值是当前最大的子序列和。这个算法还有一个有点就是,它只对数据进行一次扫描,一旦a[j]被读入处理就不需要再记忆。它是一个联机算法。
联机算法:在任意时刻算法都能够对它已读入的数据给出当前数据的解。
常量空间线性时间的联机算法几乎是完美的算法。
#include <iostream>
#include <algorithm>
using namespace std;
int s ,S , e , maxsum , cursum , len;
int maxsubsum (int a[])
{
S = 0;
maxsum = 0;
cursum = 0;
for(int i = 0 ; i < len ; i++)
{
cursum += a[i];
if(cursum < 0) {cursum = 0; S = i + 1;} // 如果小于0 起始点就在他的后面一位
if(maxsum < cursum) {maxsum = cursum; s = S ; e = i;} // s e用来记录起始点与末尾点
}
return maxsum;
}
int main()
{
int a[10];
cin >> len ;
for(int i = 0 ; i < len ; i++)
cin >> a[i];
int k = maxsubsum(a);
cout << k << endl ;
cout << s << ' ' << e << endl ;
return 0;
}
2.最长公共子序列(可不连续)
//最长公共子序列(可不连续)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int dp[105][105]; //记录当前字母“前面”的最长子序列的长度
char a[100], b[100];
int path[150];
int main()
{
while(cin >> a >> b)
{
int len1 = strlen(a);
int len2 = strlen(b);
for(int i = 1; i <= len1; i++) //i,j从一开始
for(int j = 1; j <= len2; j++)
{
if(a[i-1] == b[j-1]) //前一个相同,当前的就是前面dp+1;
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
cout << dp[len1][len2] << endl;
//输出路径,只会输其中一条;从a里面输出,可以在加上从b里面的
int i = len1, j = len2, k = dp[len1][len2]; //k代表最长的数量;ij都是从后往前找;
while(i > 0)
{
if(dp[i][j] == dp[i-1][j]) i--; //看a当前字母与前一个字母的dp是否相同,相同说明i-1那个字母
else //并不是最长子序列中的一个,不相同说明是其中的一个
{
path[k--] = i - 1; //然后用一个path记录i-1,即属于最长子序列的字母的位置
i--; //可以想成,字符串0-i(前i个),跟b的dp相比,判断每段字符最后一个字母是否属于子序列
}
}
for(int i = 1; i <= dp[len1][len2]; i++) //从前往后输出
{
printf("%c",a[path[i]]);
}
cout << endl;
/*while(j > 0)
{
if(dp[i][j] == dp[i][j-1]) j--;
else
{
path[k--] = j - 1;
j--;
}
}
for(int i = 1; i <= dp[len1][len2]; i++)
{
printf("%c",b[path[i]]);
}
cout << endl;*/
}
}
3.最长公共子串(连续)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int dp[105][105]; //记录当前字母“前面”的最长子序列的长度
char a[100], b[100];
int main()
{
while(cin >> a >> b)
{
int max1 = 0, temp;
memset(dp,0,sizeof(dp));
int len1 = strlen(a), len2 = strlen(b);
for(int i = 1; i <= len1; i++)
for(int j = 1; j <= len2; j++)
{
if(a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = 0;
if(max1 < dp[i][j])
{
max1 = dp[i][j]; //纪录dp[][]中的最大值
temp = i;//纪录最长公共子串的末端在str1中的位置(也可以纪录在str2中的位置)
}
}
for(int i = temp - max1; i < temp; i++)
cout << a[i];
cout << endl;
}
}
4.LIS的三种写法
方法一: 转换成LCS问题 空间时间复杂度都比较大, 空间n^2,时间n^2+nlogn
将原数组与排好序的数组进行求LCS,得到的最长公共子序列的就是最长上子序列.
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 10005;
int a[maxn], b[maxn], dp[maxn][maxn];
int main(void)
{
int n;
while(cin >> n)
{
memset(dp, 0, sizeof(dp));
for(int i = 0; i < n; i++) scanf("%d", &a[i]), b[i] = a[i];
sort(b, b+n);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
printf("%d\n", dp[n][n]); //非严格递增,如要严格递增要对b数组去重
}
return 0;
}
方法二:动态规划 时间复杂度n^2,空间复杂度n
设f(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:
这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面
且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末
元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,
即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;
如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], ans;
int main(void)
{
int n;
while(cin >> n)
{
for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
ans = 1;
for(int i = 1; i < n; i++)
for(int j = 0; j < i; j++)
if(a[j] < a[i] && dp[j]+1 > dp[i])
dp[i] = dp[j]+1, ans = max(ans, dp[i]);
printf("%d\n", ans);
}
return 0;
}
方法三:方法二+二分 时间复杂度nlongn
在第二种算法中,在计算每一个f(i)时,都要找出最大的f(j)(j<i)来,
由于f(j)没有顺序,只能顺序查找满足aj<ai最大的f(j),如果能将让f(j)有序,
就可以使用二分查找,这样算法的时间复杂度就可能降到O(nlogn)。于是想到用一个数组
B来存储“子序列的”最大增子序列的最末元素,即有B[f(j)] = aj在计算f(i)时,在数组B中
用二分查找法找到满足j<i且B[f(j)]=aj<ai的最大的j,并将B[f[j]+1]置为ai。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1005;
int a[maxn], b[maxn], dp[maxn];
int main(void)
{
int n;
while(cin >> n)
{
for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
int Len = 1, l, m, r;
b[1] = a[0];
for(int i = 1; i < n; i++)
{
l = 1, r = Len;
while(l <= r)
{
m = (l+r)/2;
if(b[m] < a[i]) l = m+1;
else r = m-1;
}
b[l] = a[i];
if(l > Len) Len++;
}
printf("%d\n", Len);
}
return 0;
}
上面的代码都是只求了最长的长度,但没有求路径。路径可能有多条,题目不同要求输出的不同,贴一下输出第一条
路径和最后一条路径的代码。输出第一条符合的路径:
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], pre[maxn], path[maxn], ans, e;
int main(void)
{
int n;
while(cin >> n)
{
for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1, pre[i] = -1;
ans = 1; //初始最大长度为1,结尾在a[0]
e = 0;
for(int i = 1; i < n; i++)
for(int j = 0; j < i; j++)
if(a[j] < a[i] && dp[j]+1 > dp[i])
{
dp[i] = dp[j]+1;
pre[i] = j; //记录每个点i的上一个最长序列,存在已他自己为下标的pre里
if(dp[i] > ans) ans = dp[i], e = i; //因为是>,而不是>=所以最后一个元素肯定是所有里面最前面的
}
printf("%d\n", ans);
//路径要逆推回去
for(int i = 0, k = ans; i < ans; i++)
{
path[k--] = a[e]; //从后往前推,最后一个节点为e;把他的值a【e】输出了
e = pre[e]; //pre【e】存了上一个他的节点,也就是他之前最长的长度的最后一个字母
}
for(int i = 1; i <= ans; i++)
{
if(i-1) printf(" ");
printf("%d", path[i]);
}
printf("\n");
}
return 0;
}
输出最后一条路径
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], path[maxn];
int main(void)
{
int n, ans;
while(cin >> n)
{
ans = 0;
for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
for(int i = 1; i < n; i++)
for(int j = 0; j < i; j++)
if(a[j] < a[i] && dp[j]+1 > dp[i])
dp[i] = dp[j]+1, ans = max(ans, dp[i]);
printf("%d\n", ans);
for(int i = n-1, j = ans; i > -1; i--) //从后往前找,第一个找的肯定是最后面那个最长的最后一个元素
if(dp[i] == j) path[j--] = a[i];
for(int i = 1; i <= ans; i++)
{
if(i != 1) printf(" ");
printf("%d", path[i]);
}
printf("\n");
}
return 0;
}
//求原串与逆序串的最大公共子序列(滚动数组)
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], path[maxn];
int main(void)
{
int n, ans;
while(cin >> n)
{
ans = 0;
for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
for(int i = 1; i < n; i++)
for(int j = 0; j < i; j++)
if(a[j] < a[i] && dp[j]+1 > dp[i])
dp[i] = dp[j]+1, ans = max(ans, dp[i]);
printf("%d\n", ans);
for(int i = n-1, j = ans; i > -1; i--) //从后往前找,第一个找的肯定是最后面那个最长的最后一个元素
if(dp[i] == j) path[j--] = a[i];
for(int i = 1; i <= ans; i++)
{
if(i != 1) printf(" ");
printf("%d", path[i]);
}
printf("\n");
}
return 0;
}