这道题目在poj和zoj上都有,分别是poj 1093 zoj 1147
题意:输入行宽,输入一篇文章,多行,多单词,然后以一个空行来表示文章的结束,但输入的行宽为0的时候表示程序结束。一个单词的长度不会超过行宽。然后通过调整使整片文章看起来最和谐,其实的要求是,一行中,如果不止一个单词,那么行首和行尾都必须放单词,即行首和行尾不能有空格。特殊情况是一行只放一个单词,那么这个单词一定要放在行首,另外如果这个单词的长度小于行宽,那么这一行的badness(一个数值)就为500,如果这个单词刚好等于行宽即完全放满,那么badness为0。另外badness原本是一个数值要计算的,即两个单词之间的空格数减1的平方(c-1)^2,我们的目标是通过调整单词的位置使到整片文章的badness值的和最小
先说说注意的问题,输入的问题,我就是WA在这里,当时在UVA一直WA,所有搞出来的数据都是对的,然后放到POJ是PE,这让我有了信心,至少我的程序算法是对的,但是为什么PE我怎么改都改不出来,后来发现ZOJ也有这题,又拿去ZOJ提交,又是WA,然后我就奇怪了,在烦躁的情况下我胡乱输入一些数据,就在这个乱来的过程中发现了BUG,那就是输入是任意的,即单词和单词之间不一定只有一个空格,一篇文章的开头不一定一出来就是单词而是先搞几个空格再出单词,而文章的最后不是立马结束,而是再加一个多余的空格,就是因为忘记处理这些空格只是按照常规的思维也输入数据所以一直找不到BUG(其实处理这个问题注意这种问题应该是ACMer的基本素质,不知道为什么当时没有想到)。所以处理好这些问题之后我们就可以开始DP并开始构建路径了。
保存所有的单词在w[][]数组中,每个单词的单词的长度保存在len[]数组中
DP(用记忆化搜索实现):dp[i][j]表示第i个单词位于一行的第j个位置时的最优解(这里,单词的计数是从1到n,位置是从1到L,L是行宽)。当第i个单词放下去第j个位置之后,就可以去枚举第i+1个单词可以放的位置,显然第i+1的单词能放的第一个位置是 c1=j+len[i]-1+2 (即在第i个单词后面空一个空格再放第i+1个单词), 能放的最后的位置是 c2=L+1-len[i+1] (即第i+1个单词位于行尾),这时候你会发现一个问题,可能根本放不下第i+1个单词,即当c1>c2的时候是放不下第i+1个单词的,那需不需要独立做一个判断呢,不需要的,因为我们是通关过一个循环来枚举第i+1个单词能放的位置,如果第i+1个单词放不下的,这个循环不会执行 for(k=c1; k<=c2; k++) ,若c1>c2循环不会执行
然后要判断一些特殊情况
1.例如当前的第i个单词已经位于行尾了,那么不用去枚举第i+1个单词的位置,而应该直接递归到第i+1个单词放在下一行的行首的情况
2.另一种情况,第i个单词位于行首,那么还是要去枚举第i+1个单词放在这一行的位置,除此之外,还有枚举一个特殊情况,这行就放第i个单词,第i+1个单词放下一行的行首,注意,一个单词放一行,当且仅当这个单词刚好是为位于行首时才能执行
3.可能你还会想说另一个特殊情况,一个单词位于行首,同时又位于行尾,即这个单词正好在这一行首,而且长度就是行宽,这个情况我们是怎么处理的呢?要不要独立处理呢,不用的,这种情况是归为这个单词位于行尾那种情况来处理的,即第1种特殊情况。为什么,我们在后面说到
来看看状态转移方程
dp[i][j]=dp[i+1][1] (这种情况当且仅当第i个单词位于行尾,那么第i+1个单词不用考虑只能放下一行的行首)
dp[i][1]=dp[i+1][1]+500 (这种情况当且仅当第i个单词位于行首并且长度小于行宽,然后它单独放一行那它产生的badness值是500,然后第i+1个单词放一行行首)
dp[i][j]=dp[i+1][k]+b (k是第i+1个单词放在这行的位置,b是第i个单词和第i+1个单词之间的空格计算出来的badness值)
这是状态转移方程的三种可能,选值最小的,而这三个判断的优先顺序其实是有些讲究的
首先应该判断第i个单词是否位于行尾,因为位于行尾的话,接下来的递归只有一种情况,就是第i+1个单词放下一行的行首
即dp[i][j]=dp[i+1][1] , 递归结束后就应该直接返回上一层了
然后去判断这个单词是否位于行首,如果位于行首,才能进行这个递归
即dp[i][1]=dp[i+1][1]+500 , 第i个单词单独放一行。返回后,不能直接再返回上一层,还要继续去枚举第i+1个单词放在当前这行的情况
即dp[i][j]=dp[i+1][k]+b
所以说,一个单词如果位于行首并且也位于行尾即长度等于行宽的时候,当做位于行尾的情况来处理,我们比较两者
dp[i][j]=dp[i+1][1]
dp[i][1]=dp[i+1][1]+500
后者还要加上500,而我们是求较小值,显然上面的更优,有了上面的就不要下面的了,当然你颠倒顺序是不影响程序的正确性的,因为还是会被修改回来
然后就是记录路径的问题,path[i][j]表示当第i个单词位于一行的第j个位置时它后面有多少个空格
如果第i个单词位于行尾,那么它后面的空格数是0,即path[i][j]=0
如果第i个单词位于行首并且单独放一行,它后面的空格也是0,因为这是题目说明的,如果一个单词单独一行它后面不需要输出空格,所以也是path[i][j]=0
所以当我们遇到path[i][j]=0的时候就直接输出这个单词并且换行,然后下一个状态就是path[i+1][1],如果不为0的话,就要计算出下一个状态
path[][]数组初始化为-1
然后就说这么多,看代码吧
#include <stdio.h> #include <string.h> #define INF 1000000000 #define LEN 90 #define MAXN 10010 char w[MAXN][LEN]; int len[MAXN]; int f[MAXN][LEN]; bool visit[MAXN][LEN]; int path[MAXN][LEN]; int L,n; /* //这个函数没用,只是为了检验处理输入后的结果是否正确 void print_input() { int i; printf("单词个数:%d\n",n); for(i=1; i<=n; i++) printf("%s %d\n",w[i],len[i]); return ; } */ int input() { int i,j,flag; char temp[MAXN]; gets(temp); sscanf(temp,"%d",&L); if(!L) return 0; n=1; while(1) { gets(temp); if(temp[0]=='\0') break; for(flag=0,j=0,i=0; i<strlen(temp); i++) { if(temp[i]==' ' && !flag) continue; else if(temp[i]==' ' && flag) { flag=0; w[n][j]='\0'; len[n]=strlen(w[n]); j=0; n++; } else if(temp[i]!=' ' && flag) w[n][j++]=temp[i]; else if(temp[i]!=' ' && !flag) { flag=1; j=0; w[n][j++]=temp[i]; } } if(flag) { w[n][j]='\0'; len[n]=strlen(w[n]); n++; } } n--; return 1; } void print_path(int i , int j) { int t,k,c; if(i==n) { printf("%s\n",w[i]); return ; //递归边界 } if(path[i][j]==0) //说明输出这个单词并且直接换行 { t=1; //下一个单词的开始位置 printf("%s\n",w[i]); print_path(i+1,t); return ; } else if(path[i][j]!=-1) { c=path[i][j]; //空格数 t=j+len[i]-1+c+1; //下一个单词开始的位置 printf("%s",w[i]); while(c) { printf(" "); c--;} //输出空格 print_path(i+1,t); return ; } return ; } int dp(int i , int j) { int ans,c,k,b; if(visit[i][j]) return f[i][j]; visit[i][j]=1; f[i][j]=INF; if(i>n) //递归边界 { if(j==1) //说明第n个单词是在行尾的 return f[i][j]=0; else //说明第n个单词并不是在行尾而是在行中,这是不允许的 return f[i][j]=INF; } if( j+len[i]-1 == L) //若单前的单词位于行尾,那么直接递归下一个单词在行首 {//一个单词位于行尾它可能同时是位于行首的,即一个单词完整占据一行,但这样并不符合赋值badness=500的条件 //是当单词单独位于一行的行首,且单词长度小于行宽时才赋值badness=500 //所以从逻辑上的优先性来讲,要找最小值,应该先判断是否位于行尾这样就可以不用再判断是否位于行首了 ans=dp(i+1,1); if(ans<f[i][j]) { f[i][j]=ans; path[i][j]=0; //我们用path[i][j]=0来表示第i个单词是位于行尾的,不需要输出空格,并且输出单词后要换行 } return f[i][j]; //可以返回了不需要做其他的递归因为只有一种递归的可能 } if(j==1) //说明这个单词位于行首且长度一定比行宽小,否则的话上面那次递归已经返回上一层了不会有这个判断 {//尝试把这个单词单独放一行去递归 ans=dp(i+1,1)+500; if(ans<f[i][j]) { f[i][j]=ans; path[i][j]=0; //这里我们同样用0来表示位于行首的且单独放一行的情况 //因为单独放一行的时候单词的后面不需要补空格并且输出单词后要换行,和位于行尾是一样的 } } //另外要递归,在这个单词后面继续放下一个单词 c=j+len[i]-1+2; //下一个单词最开始可以放的位置 for(k=c; k<=L+1-len[i+1]; k++) //枚举所有可以放下一个单词的位置 { b=k-c; ans=dp(i+1,k)+b*b; if(ans<f[i][j]) { f[i][j]=ans; path[i][j]=b+1; //保存空格数 } } return f[i][j]; } void solve() { int i,j; int ans; memset(path,-1,sizeof(path)); memset(visit,0,sizeof(visit)); len[n+1]=0; ans=dp(1,1); // printf("ans=%d\n",ans); //输入最后的badness总和,只是题目不需要输出,可以用于检查程序是否正确 print_path(1,1); printf("\n"); } int main() { while(input()) { // print_input(); solve(); } return 0; }