DP 算法总结


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一定是已经循环里面遍历过了。主要是观察递推方程的前后关系,设计循环保证子问题已经之前算出来了。



最大最小合并都是DP,原问题优,子问题必优,而且子问题肯定不互相影响,
#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]+1, 而不是max(c[i-1][j],c[i][j-1], c[i-1][j-1]+1)

沈老师进行了一番证明,虽然有个细节没理解,但是 后来看了下教材发现教材其实已经给出了证明,用反证法,说c[i-1][j-1]+1就是c[i][j]搜索空间的最优值。

另外犹记得沈老师说过DP是有点难的算法,关键是证明是DP问题,然后推递推方程。

总结一些错误,起始情况一定要想清楚,不要盲目付0,然后递推方程一定要因地制宜,不要跟风,小标一定要准确,不要越界,然后代码一定要保证子问题都是之前循环已经算出来的值,目前只有矩阵链要稍微注意,外层length循环


今天写一道三角形最短路径居然掐了一会儿,实在不能理解,后来发现是递推方程没有完全想清楚就开始写,导致后面代码手动模拟一个例子居然出错,另外还有一点就是用vector可以让你屏蔽使用数组一些下标的细节,但是一定要记得clear,因为他会一直push的。
递推方程: 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;
    }













评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值