01背包也是之前联系过的,最长公共子序列(和最短编辑举例一类),还有石子合并问题(本质是矩阵链乘法一类的)
今天写了一下最长公共子序列,发现了问题,我习惯了用string于是用这个容器来装字符序列,然后就出现了问题。
代码ac,递推方程必须全队,包括条件,下标。因为子串长度问题,必须m+1 n+1空间,in位长度1和j的lcs可能借助长度0和长度j的lcs,因此定义c(0,j)表示空串
与长为j的串的lcs,值为0,也即空串与任意串没有公共子序列 ,c(i,0)同理。
所以c(i,j)的准确含义是从起点开始长为i和长为j的串的LCS的长度。我当时没有想清楚c[][]的准确含义,只申请了m n的空间,以为原来的 c[0][j]只是0-base 和1-base的区别,但是还有其他意义。
所以我的代码愣是吧长为1字串与其他船的LCS的循环单独列出来,而且极容易出错,所以还是教材版本,不要用0-base 的string,用个char* 来处理,而且string的length还不可写,length是private成员。
c(i,j)=max{c(i-1,j),c(i,j-1),c(i-1,j-1)+1} 如果x(i-1)=y(j-1)max里面才包含第三项 i=1...m, j=1....n
c(i,0)=0, i=0....m
c(0,j)=0, j=0....m
对于上面状态方程,教材写的是如果相等 max只有第三项,不等max里前两项,我写的是相等也包含前两项,难道可以证明相等时第三项一定比前两项大?
后来重新过了一遍证明,发现其实对于c(i-1,j) c(i,j-1) 并没有用到xi-1 !=yi-1的条件,因此当xi-1==yi-1 的时候,应该把 c(i-1,j) c(i,j-1)加进来。不大清楚为啥教材没加。
所以思考的过程,包含准确定义并理解c[][], 完整写出递推方程,下标,起始case等等。
代码实现,这步只考察代码熟练度,没有C++多大要求,但是我经常写的不够专心,总是一些写错最后debug发现写的代码和递推方程都不一样,往往是下表的错误,几乎每次都要翻这个错,浪费了时间,我放弃治疗了= =
附上我写的 c[][] m*n空间的复杂代码,引以为鉴
#include <string>
#include <iostream>
using namespace std;
void f(int**c, string x, int m, string y, int n)
{
if(x.at(0)==y.at(0))
c[0][0]=1;
else
c[0][0]=0;
int max;
for(int i=1;i<=m-1;i++)
{
//c[i][0]
max=c[i-1][0];
if(x.at(i)==y.at(0))
if(max<1)
max=1;
c[i][0]=max;
}
for(int j=1;j<=n-1;j++)
{
max=c[0][j-1];
if(x.at(0)==y.at(j))
if(max<1)
max=1;
c[0][j]=max;
}
for(int i=1;i<=m-1;i++)
{
for(int j=1;j<=n-1;j++)
{
int max=c[i-1][j];
if(max<c[i][j-1])
max=c[i][j-1];
if(x.at(i)==y.at(j))
{
if(max<(c[i-1][j-1]+1))
max=c[i-1][j-1]+1;
}
c[i][j]=max;
}
}
}
int main()
{
int casen,m,n;
cin>>casen;
int casei=1;
while(casei<=casen)
{
cin>>m>>n;
string x,y;
char ch;
for(int i=0;i<m;i++)
{
cin>>ch;
x+=ch;
}
for(int j=0;j<n;j++)
{
cin>>ch;
y+=ch;
}
int **c=new int*[m];
for(int i=0;i<m;i++)
c[i]=new int[n]();
f(c,x,x.length(),y,y.length());
cout<<"Case "<<casei<<endl<<c[m-1][n-1]<<endl;
casei++;
/*
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
cout<<c[i][j]<<" ";
}
cout<<endl;
}*/
for(int i=0;i<m;i++)
delete[] c[i];
delete[] c;
}
}
石子合并问题其实状态空间很像矩阵链,我感觉遇到的多数DP问题状态空间就是矩阵链空间c[i][j]和汽车装配c[i]这两类。。。。
这道题目一看,如果从某处断开,其实是矩阵链,但是有环,所以当成N次矩阵链,然后max取最大,时间复杂度有点高,O(n^4) 但是OJ居然让我AC了。。。。
大概计算了一下时间复杂度,长为2, n-1个....长为n-1 1个, 长为2的断链循环是1次,长为n-1的断链循环是n-2次,因此时间复杂度 1(n-1)+2(n-2)+....(n-1)*1 结果这个都忘记算了,数列求和都交还给敬爱的李美平老师了。。。。
c[i][j]=max(k) (cik+ck+1j+sum a[i...j]), 1<=i<j<=n, i<=k<=j-1
cii=0 i=1....n
我今天coding过程中终于明白为啥外层循环要用length,然后里层start 或者end,嘴里曾先计算end 或者start, 再来断链位置的循环了。还记得当时融神看到矩阵链那道题目也是一样觉得很费解。。。
因为递推方程要保证其中分解的子问题必须已经正确解出了,就像递归,递归调用是先调用再逐步到底返回来解决的。我已开始写的是for i 1->n-1 for j i+1->n for k i->j-1
后来出bug了,因为不能保证每次子问题都已解除,可能还没解出来呢。怎么保证呢,因为子问题的长度一定比原问题小,<=原问题长度-1,因为断链位置N-1个嘛,所以外层长度从2->n 一定可以保证子问题都解除了。
所以代码必须是for l 2->n-1 for i 1->n-1 j=i+l-1 for k i->j-1
为啥前面的LCS不需要考虑这个问题呢?
其实是考虑了,因为ij从小到大,保证了小标小的之前已经算出来了,i-1j ij-1一定是已经循环里面遍历过了。主要是观察递推方程的前后关系,设计循环保证子问题已经之前算出来了。
#include <iostream>
using namespace std;
int sum(int *a, int s, int e)
{
if(s>e)return 0;
int sum=0;
for(int i=s;i<=e;i++)
sum+=a[i];
return sum;
}
void MaxStoneMerge(int **c, int *a, int n)//c[1..n]
{
//initial
for(int i=1;i<=n;i++)
c[i][i]=0;
//recursive formular
for(int l=2;l<=n;l++)//length from 2 to n, ensure lower length has been computed
{
for(int i=1;i<=n-1;i++)//start index
{
int j=i+l-1;//get end index according start index and length
if(j>n) continue;
int max=c[i][i]+c[i+1][j]+sum(a,i,j);
for(int k=i+1;k<=j-1;k++)
{
if(max<c[i][k]+c[k+1][j]+sum(a,i,j))
max=c[i][k]+c[k+1][j]+sum(a,i,j);
}
c[i][j]=max;
}
}
/*
for(int i=1;i<=n-1;i++)
{
for(int j=i+1;j<=n;j++)
{
int max=c[i][i]+c[i+1][j]+sum(a,i,j);
for(int k=i+1;k<=j-1;k++)
{
if(max<c[i][k]+c[k][j]+sum(a,i,j))
max=c[i][k]+c[k][j]+sum(a,i,j);
}
c[i][j]=max;
}
}
*/
}
void MinStoneMerge(int **c, int *a, int n)//c[1..n]
{
//initial
for(int i=1;i<=n;i++)
c[i][i]=0;
//recursive formular
for(int l=2;l<=n;l++)//length from 2 to n, ensure lower length has been computed
{
for(int i=1;i<=n-1;i++)//start index
{
int j=i+l-1;//get end index according start index and length
if(j>n) continue;
int min=c[i][i]+c[i+1][j]+sum(a,i,j);
for(int k=i+1;k<=j-1;k++)
{
if(min>c[i][k]+c[k+1][j]+sum(a,i,j))
min=c[i][k]+c[k+1][j]+sum(a,i,j);
}
c[i][j]=min;
}
}
/*
for(int i=1;i<=n-1;i++)
{
for(int j=i+1;j<=n;j++)
{
int max=c[i][i]+c[i+1][j]+sum(a,i,j);
for(int k=i+1;k<=j-1;k++)
{
if(max<c[i][k]+c[k][j]+sum(a,i,j))
max=c[i][k]+c[k][j]+sum(a,i,j);
}
c[i][j]=max;
}
}
*/
}
void ShitfLeft(int *a, int n)//a[1..n] left shift
{
//int tmp=a[1];
if(n<=0) return;
for(int i=1;i<=n;i++)
a[i-1]=a[i];
a[n]=a[0];
}
int main()
{
int n;
while(cin>>n)
{
int *a=new int[n+1];
for(int i=1;i<=n;i++)
cin>>a[i];
int **c=new int*[n+1];
for(int i=0;i<=n;i++)
c[i]=new int[n+1];
//original max, min
MaxStoneMerge(c,a,n);
int max=c[1][n];
MinStoneMerge(c,a,n);
int min=c[1][n];
//n-1 left shift
for(int i=1;i<=n-1;i++)
{
ShitfLeft(a,n);
MaxStoneMerge(c,a,n);
if(max<c[1][n])
max=c[1][n];
MinStoneMerge(c,a,n);
if(min>c[1][n])
min=c[1][n];
}
cout<<min<<" "<<max<<endl;
/*
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
cout<<c[i][j]<<" ";
cout<<endl;
}
*/
for(int i=0;i<=n;i++)
delete[] c[i];
delete[] c;
delete[] a;
}
return 0;
}
另外还有一道最短编辑举例,我居然弄了那么久,最后还是自己发现了问题,我递推方程的都写错了= =
我是一个不喜欢看答案的人,尽管一翻教材就知道问题在哪,但是就是自己思索解决,记得前几年一个shanghai高考状元,华师大二附中的学生说我不喜欢看答案,只要有时间,我一定要先仔细思考,各个都考虑,实在不行才看答案,这个也是一直鞭策我,我也要这样,只可惜留给我的时间已经不多了~~~~
我已开始写的是min(c[i-1][j]+1, c[i][j-1]+1, c[i-1][j-1]+1) 如果xi!=yj min只有前两项,否则有前三项,完全按照最长公共子序列的模板搬过来,稍微改改。
后来答案怎么都不对,而且对于出口还犯了错误,默认以为都是0,完全不思考为0的含义,这都是有数学含义的,不是找模板套的。
c[i][0]=i i=0..m 表示长为i的串与空串MED为i,c[0][j]=j j=0..n 我已开始还漏了i=0,j=0的情况,其实就是第一行第一列清0,有一个重复的就是c00
后来发现自己犯了这个错误,如果xi=yj,那么编辑距离不用+1, 而是直接c[i-1][j-1] LCS是要加1的。没有因地制宜,理解本质,犯了当时insertsort从数组到linklist迁移的老毛病。
正确的递推方程是 c[i][j]=min(c[i-1][j]+1, c[i][j-1]+1, c[i-1][j-1]) if xi=yj
=min(c[i-1][j]+1, c[i][j-1]+1, c[i-1][j-1]+1) if xi!=yj
另外还有一个C++错误,在OJ上,因为还要处理空串与其他串的情况,我直接cin>>string, 结果发现不能赋空串,一定要有输入才能继续进行。
因此我模模糊糊用个getline(cin,string,'\n'), 结果发现就可以敲换行符就赋予空串,然后就AC了。
犹记得大概去年李炜师兄大神在admis做SHUOJ的时候一道题目过不了,CX fawks大神还有我这个菜鸟一起围观,结果fawks大神立马发现了就是类似一样的问题,好像是换了getline函数,直接截取空串,然后大神还解释了一翻原因,并说明了OJ的一个bug。
我居然一年后才遇到这个问题,并解决了这个问题,这太可怕了= =
在有限的几个月时间,我的C++ DS Alg可以达到fawks的水平么,这真的要打上一个大大的问号,烟酒生的生涯读的那堆论文,写的低质量的代码,用的别人的包,使我的代码能力急剧下降,我要加油!
#include <iostream>
#include <string>
using namespace std;
int Min(int a, int b, int c)
{
int min=a;
if(min>b)
min=b;
if(min>c)
min=c;
return min;
}
void f(int **c, string x, int m, string y, int n)
{
for(int i=0;i<=m;i++)
c[i][0]=i;
for(int j=0;j<=n;j++)
c[0][j]=j;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
/*
int min=c[i-1][j]+1;
//if(x.at(i-1)!=y.at(j-1))
//{
if(min>(c[i][j-1]+1))
min=c[i][j-1]+1;
//}*/
if(x.at(i-1)==y.at(j-1))
{
c[i][j]=Min(c[i-1][j]+1,c[i][j-1]+1,c[i-1][j-1]);
//if(min>(c[i-1][j-1]))
// min=c[i-1][j-1];
}
else
{
c[i][j]=Min(c[i-1][j]+1,c[i][j-1]+1,c[i-1][j-1]+1);
}
//c[i][j]=min;
}
}
}
int main()
{
string x,y;
while(getline(cin,x,'\n'))
{
getline(cin,y,'\n');
int **c=new int*[x.size()+1];
for(int i=0;i<=x.size();i++)
c[i]=new int[y.size()+1];
f(c,x,x.size(),y,y.size());
cout<<c[x.size()][y.size()]<<endl;
/*
for(int i=1;i<=x.size();i++)
{
for(int j=1;j<=y.size();j++)
cout<<c[i][j]<<" ";
cout<<endl;
}*/
for(int i=0;i<=x.size();i++)
delete[] c[i];
delete[] c;
}
return 0;
}
今天和ACM教练沈老师交流,讨论为啥LCS问题当xi=yj时候,c[i][j]=c[i-1][j-1]
沈老师进行了一番证明,虽然有个细节没理解,但是 后来看了下教材发现教材其实已经给出了证明,用反证法,说c[i-1][j-1]+1就是c[i][j]搜索空间的最优值。
另外犹记得沈老师说过DP是有点难的算法,关键是证明是DP问题,然后推递推方程。
总结一些错误,起始情况一定要想清楚,不要盲目付0,然后递推方程一定要因地制宜,不要跟风,小标一定要准确,不要越界,然后代码一定要保证子问题都是之前循环已经算出来的值,目前只有矩阵链要稍微注意,外层length循环
递推方程: dp[i][j]=min(dp[i-1][j-1],dp[i-1][j])+a[i-1][j-1], dp[0][0]=0,还是要注意里面最好表示1-base的状态,也即多出dp[0][0]作为特殊的用法,但是我已开始
以为只有dp[i][1]的时候,要特殊处理,dp[i][j]=dp[i-1][j]+a[i-1][j-1], 却忘了最后一个数也要特殊处理的,dp[i][i]时候,dp[i][j]=dp[i-1][j-1]+a[i-1][j-1], 两个正好对应嘛,左列其实不需要0列的,但是需要0行,
例如递归构造解的时候需要,这是看了ahdoc代码后的想法,其实这个本应该掌握,只是写的不熟练导致每次都有些细节可能会出问题,写多了就好了。
int minimumTotal(vector<vector<int> > &triangle) {
int n=triangle.size();
if(n==0) return 0;
//int dp[][];
vector<int> lastrow,currow;
lastrow.push_back(0);
//dp[0][0]=0;
for(int i=1;i<=n;i++)
{
currow.clear();
for(int j=1;j<=i;j++)
{
if(j==1)
currow.push_back(lastrow[j-1]+triangle[i-1][j-1]);
//dp[i][j]=dp[i-1][j]+triangle[i-1][j-1];
else if(j>=2 && j<=i-1)
{
currow.push_back(min(lastrow[j-1],lastrow[j-2])+triangle[i-1][j-1]);
//dp[i][j]=min(dp[i-1][j],dp[i-1][j-1])+triangle[i-1][j-1];
}
else if(j==i)
{
currow.push_back(lastrow[j-2]+triangle[i-1][j-1]);
}
}
lastrow=currow;
}
int min=currow[0];
for(int i=1;i<currow.size();i++)
if(min>currow[i]) min=currow[i];
//cout<<endl;
return min;
}