homework-02
回答问题
描述在这么多相似的需求面前, 你怎么维护你的设计 (父类/子类/基类, UML, 设计模式, 或者其它方法) 让整个程序的架构不至于崩溃的?
答:处理这种问题显然还是面向过程比较方便,像这种写完了测试通过了就扔的代码没有必要去维护其扩展性,重用性。更别说什么uml父类子类繁琐的东西,用最简单的方法解决问题就好了。如果是web应用或者桌面应用才需要曲考虑这些所谓维护设计吧。
给出你做单元测试/代码覆盖率的最终覆盖率的报告, 用截屏显示你的代码覆盖率
答:不会用VS,在linux下用了lcov来生成覆盖率,并成生了结果html,详见github上的maxsum.c.gcov和resu/index.html。
阅读 工程师的能力评估和发展 和相关文章, 在完成作业的时候记录自己花费的时间, 并填下表。如果你对有些术语不太清楚,请查看教材和其它资料。如果你认为你不需要做某个步骤, 那就跳过去。
答:跳过。
你在这个作业中学到了什么? 有什么好的设计值得分享? 感想如何 (太容易 / 太难 / 太无趣)?
答:学了一下Markdown来写文档,表示很不错。重新复习了一下单调队列。对于第三问的感想:太难。
问题重述
这次作业略繁琐。需要在一个程序内支持传递运行参数,支持横向相通,纵向相通,扩展子区域(随意联通)。
问题分析
需要重新构造程序,支持传入参数,有-v-h-a三种,如果是c语言的话,在main()
函数内加int argc,char *argv[]
就可以引用传入的参数,其中文件名在argv[argc-1]
里,程序名在argv[0]
里,剩下的就是支持的选项。
只有-v和-h怎么做
-v是上下联通,-h是左右联通。对于环形问题很容易想到的方法就是复制一份接在矩形后边,比如如果需要支持左右联通-h,那么如果原来的矩形是:
5 6 -3 8 -9 2
则可以往右边复制一份自己接上,变成
5 6 -3 8 -9 2 | 5 6 -3 8 -9 2
这样就能够包含所有环形结构,不过有个问题,这样求出来会包含长度大于m的结果,而这是不符合题意的。
为了避免这样的问题,从之前的解法很难再改进。
另外-v只需要限制上下区间的距离就能避免不合题意的情况,所以不用特殊处理。
另一种思路
换一种思路去考虑这个问题就比较容易了,令sum[i]
表示sigma{a[j],1<=j<=i}
(其中sum[0]=0
),我们的目的是最大化sum[a]-sum[b]
,即:
ans=Max{sum[a]-sum[b],0<=b<=a<=m}
从左向右循环,记录最小的sum[min]
,对于每个i
,以a[i]
结尾的最大和的子串就是sum[i]-sum[min]
,所以只需要不断更新sum[min]
并计算最大的sum[i]-sum[min]
就行了。时间复杂度也是O(N)
。
为了限制长度i-min<=m
,可以维护一个队列dd[]
,其中队头是t
,队尾是w
。其中dd[i].value
是其sum
值,而dd[i].id
是该sum
值对应的i
。
维护队列t-w按dd[].value
从小到大排序,每次取出dd[t]
便为需要的sum[min]
。而每次计算完一个sum
值需要插入到队尾。
若dd[t].id
距离当前i大于m,也就是取得了不符合规定的区间,那么该dd[t]
就不能被使用,显然该值同样不能被大于i的sum使用,所以应该让其出队。
新加入的sum[i]
到dd[w]
中需要做一次插入排序,而显然所有大于sum[i]
的队列里的元素都应该被去除。
所以在一次循环中,每个点只会进入单调队列一次,而且再被插入的时候就会被淘汰。所以维护单调队列不会带来复杂度的增加,仅会带来常数的增加。所以时间复杂度依然为O(n^2*m)
扩展子区域 -a如何实现
大概想了一下可以用状态压缩动态规划来做,这样可以得到最优解。
最朴素的想法是用1表示选定格子,0表示不选定格子,一行一行进行递推,只有上面的状态和下面的状态有联通才能转移。
具体一点说,用Dp[i][status]来表示第i行,状态为status的方格,其取到的最优子区域和是多少。用Canappend(status1,status2)来表示两个状态是否能转移。则动态规划方程为:
Dp[i][status]=Max{Dp[i-1][status_i],status_i in status_all and Canappend(status,status_i)}+cost[i][status]
其中cost[i][status]为取第i行中status的状态所带来的和。
这种想法很简单,不过可惜是错的。
很明显,状态数是不够的,因为单单有0和1表示的信息不够,它只能表示纵向联通的信息,而不能表示横向联通,这样最直接的结果就是终状态的合理性无法判断,所以要正确进行Dp,还需要额外的信息量。
用0-k表示格子的选取情况,其中0表示不取,1-k表示其处于某种联通状态。比如说
0 1 1 2 0 2 1
表示其中{2,3,7}为一个联通块(不管它到底怎么连的,反正在它上边就是连上了),{4,6}是另一个联通块。这样表示的信息就足够了。
但信息变多带来的是转移的复杂性,一行一行转移所需要处理的状态有点略多。
据说可以用插头Dp来实现简单转移,我看了下一篇论文,发现实现起来还是有点微难的。其复杂度上界大概是O((m/2)^m*n),在n,m<=32的情况下的运行时间依然是天文数字。所以任务很艰难,在下能力有限,还是没能写出正解来。
另外有一种想法是先把所有联通的正值点抽象成一个点,然后再在这些新的"大点"上作最小生成树,不过这显然没法得到最优解。
程序
程序只实现了-h -v两种参数。
1 #include<stdio.h> 2 3 #define MAXN 1000 4 #define MAXM 1000 5 #define MINV (-2147483647) 6 int n,m,a[2*MAXN+2][2*MAXM+2],dd[2*MAXM+2],id[2*MAXM+2]; 7 8 void input(char *cin) 9 { 10 int i,j; 11 freopen(cin,"r",stdin); 12 scanf("%d,",&n); 13 scanf("%d,",&m); 14 for(i=0;i<n;i++) 15 for(j=0;j<m;j++) 16 { 17 scanf("%d",&a[i][j]); 18 getchar(); 19 } 20 } 21 int donocir() 22 { 23 int i,j,k,sum,ans; 24 ans=MINV; 25 int p[MAXM+2]; 26 for (i=0;i<n;i++) 27 { 28 for(k=0;k<m;k++) p[k]=0; 29 for (j=i;j<n;j++) 30 { 31 sum=0; 32 for (k=0;k<m;k++) 33 { 34 p[k]+=a[j][k]; 35 if (sum<0)sum=0; 36 sum+=p[k]; 37 if (sum>ans) 38 ans=sum; 39 } 40 } 41 } 42 return ans; 43 } 44 int docir(int ish,int isv) 45 { 46 int i,k,j,l,p[2*MAXM+2],nn,mm,ans=MINV,sum,t,w,ai,aj,ak,at; 47 nn=n; mm=m; 48 for(i=0;i<n;i++) 49 for(j=0;j<m;j++) 50 { 51 if(ish) 52 { 53 a[i][j+m]=a[i][j]; 54 } 55 if(isv) 56 { 57 a[i+n][j]=a[i][j]; 58 a[i+n][j+m]=a[i][j]; 59 } 60 } 61 if(isv)nn=2*n; else nn=n; 62 if(ish)mm=2*m; else mm=m; 63 for(i=0;i<n;i++) 64 { 65 for(k=0;k<mm;k++) p[k]=0; 66 for(j=i;j<nn && j<i+n;j++) 67 { 68 t=0; 69 w=0; 70 dd[t]=0; 71 id[t]=-1; 72 sum=0; 73 for(k=0;k<mm;k++) 74 { 75 p[k]+=a[j][k]; 76 sum+=p[k]; 77 while(k-id[t] > m)t++; 78 if (sum-dd[t] > ans) 79 { 80 ans=sum-dd[t]; 81 ai=i; 82 aj=j; 83 ak=k; 84 at=id[t]; 85 } 86 while(sum<dd[w] && w>=t) w=w-1; 87 w=w+1; 88 dd[w]=sum; 89 id[w]=k; 90 } 91 } 92 } 93 printf("%d\n%d\n%d\n%d\n",ai,aj,at,ak); 94 return ans; 95 } 96 int main(int argc,char *argv[]) 97 { 98 int i,j,ish=0,isv=0; 99 if (argc==1) 100 { 101 printf("no input file!\n"); 102 } 103 if (argc==2) 104 { 105 input(argv[1]); 106 printf("%d\n",donocir()); 107 } 108 else 109 { 110 input(argv[argc-1]); 111 for (i=1;i<argc-1;i++) 112 for(j=1;argv[i][j]!='\0';j++) 113 { 114 if (argv[i][j]=='h') ish=1; 115 if (argv[i][j]=='v') isv=1; 116 } 117 printf("%d\n",docir(ish,isv)); 118 } 119 return 0; 120 }
执行方法
运行./maxsum [options] <filename>
支持 -h -v -hv 和 -h -v