条形码问题 dp+求某个序列在某种排列中的序号的方法
条形码是一种由亮条(Light Bar)和暗条(Dark Bar)交替出现且以暗条为起头的符号,每条都占有若干个单位宽。图33-1给出了一个含有4个条的条形码,它延续了1+2+3+1=7单位的宽。
一般情况下BC(N,K,M)是一个包含所有由K个条,总宽度正好为N个单位,每个条的宽度至为M个单位性质的条形码组成的集合。例如:图33-1的条形码属于BC(7,4,3)而不属于 BC(7,4,2)。 图33-2显示了集合BC(7,4,3)中的所有16个符号,其中1表示暗,0表示亮。图中所示条形码已按字典顺序排列,冒号左边数字为条形码的编号。图33-1的条形码在BC(7,4,3)的编号为4。
输入格式:
输入文件Input4.DAT的第一行为N、K、M的值(1≤N,K,M≤33)。第二行为数字S(0≤S≤100),而后的S行中,每行为一个图33-2那样描述的集合BC(N,K,M)中的一个条形码。
输出格式:
你的程序应完成任务:A、把输出内容写入文件OUPUT4.DAT。第一行是BC(N,K,M)中条形码的个数。
B、OUPUT.DAT的第二行起的S行中,每一行是输入文件对应条形码的编号;输入与输出数据中同一行相邻两个数之间用空格区分。
ans(n,k,m)=sum{ans(n-x,k-1,m)}(1<=x<=min(m,n-k+1),km>=n)(n>k)
ans(k,k,m)=1
ans(t,k,m)=0(t<k)
然后是求每个条形码的序号,这个需要一些技巧。
***记一下
举例:1101000,求序号
l[1]=2,l[2]=1,l[3]=1,l[4]=3
比其小的有:
先是l[1]<2的(ans[7-1][3]=7)
再是l[1]=2,l[2]>1的(ans[7-2-2][2]+ans[7-2-3][2]=2+1=3)
还有l[1]=2,l[2]=1,l[3]<1的(0)
还有l[1]=2,l[2]=1,l[3]=1,l[4]>3的(0)
因此其序号是7+3+0+0-1+1=10
其他的以此类推
***
#include<cstdio> #include<iostream> #include<cstring> #include<string> #include<algorithm> using namespace std; int ans[40][40]; int n,m,k,num,temp,temp2; int l[40]; int s; string s1; int ans2; int dfs(int n,int k) { if(k*m<n) { ans[n][k]=0; return 0; } if(n>k) { ans[n][k]=0; for(int i=1;i<=min(m,n-k+1);i++) if(ans[n-i][k-1]==-1) ans[n][k]+=dfs(n-i,k-1); else ans[n][k]+=ans[n-i][k-1]; } else if(n==k) ans[n][k]=1; else ans[n][k]=0; return ans[n][k]; } int main() { int i,p,j,j1; memset(ans,-1,sizeof(ans)); scanf("%d%d%d",&n,&k,&m); printf("%d\n",dfs(n,k)); scanf("%d",&s); for(i=1;i<=s;i++) { ans2=0; cin>>s1; p=0; num=0; for(j=0;j<s1.length()-1;j++) { p++; if(s1[j]!=s1[j+1]) { l[++num]=p; p=0; } } l[++num]=p+1; temp=n; temp2=k; for(j=1;j<=num;j++) { temp2--; if(j%2==1) for(j1=1;j1<l[j];j1++) { ans2+=ans[temp-j1][temp2]; } else for(j1=l[j]+1;temp-j1>=temp2&&j1<=m;j1++) { ans2+=ans[temp-j1][temp2]; } temp-=l[j]; } printf("%d\n",ans2); //1101110 //1100 } return 0; }
再贴一段其他人的代码
#include<cstdio> #include<algorithm> using namespace std; int n,k,m,s,a[100000]; long long f[1000][1000]; char c[100]; int main(){ scanf("%d%d%d%d",&n,&k,&m,&s); f[0][0]=1; for(int i=1;i<=n;i++) for(int j=1;j<=k;j++) for(int l=1;l<=min(m,i);l++) f[i][j]=f[i][j]+f[i-l][j-1]; printf("%d\n",f[n][k]); for(int i=1;i<=s;i++){ scanf("%s",c);c[n]=(c[n-1]-'0')^1+'0'; int ans=0,sum1=0,sum2=0,num=0,p=1; for(int j=1;j<=n;j++)if(c[j]!=c[j-1])a[++num]=p,p=1;else p++; for(int j=1;j<=num;j++){ sum2++; if(j&1)for(int l=a[j]-1;l>=1;l--)ans+=f[n-sum1-l][k-sum2]; else for(int l=a[j]+1;l<=m;l++)ans+=f[n-sum1-l][k-sum2]; sum1+=a[j]; } printf("%d\n",ans); } }
再贴一段讲解,并没有什么卵用:
动态规划和搜索有着千丝万缕的关系,我们先来看一个例子:
一、问题描述
“条形码”是一种由亮条(light bar)和暗条(dark bar)交替出现,且以暗条起头的符号。每个“条”(bar)都是若干个单位宽。图1给出了一个含4个“条”的“条形码”,它延续了1+2+3+1=7个单位宽。
一般情况下,BC(n,k,m)是一个包含所有由:k个“条”,总宽度正好为n个单位,每个“条”的宽度至多为m个单位的性质的“条形码”组成的集合。例如:图1的条形码属于BC(7,4,3),而不属于BC(7,4,2)。
0: 1000100 | 8: 1100100
1: 1000110 | 9: 1100110
2: 1001000 | 10: 1101000
3: 1001100
| 11: 1101100
4: 1001110 | 12: 1101110
5: 1011000 | 13: 1110010
6: 1011100 | 14: 1110100
7: 1100010 | 15: 1110110
图1 图2
图2显示了集合BC(7,4,3)中的所有16个符号。1表示暗,0表示亮。图中的条形码已按字典顺序排列。冒号左边的数字为“条形码”的编号。图1中条形码的在BC(7,4,3)中的编号为4。
输入:输入文件的第一行为数字n,k,m(1<=n,k,m<=30)。第二行为数字s (s<=100)。而后s行中,每行为一个如图1那样描述的集合BC(n,k,m)中的一个“条形码”。
输出:在输出文件中第一行为BC(n.k,m)中“条形码”的个数。而后s行中每一行为输入文件中对应“条形码”的编号。
输入输出示例:
二、分析
题目有两问。容易看出,计数是求序号的基础,因此我们先解决计数问题。原题只给了一个实例,即条形码。为了能用计算机解决该问题,我们必须先建立一个能够很好描述该问题的数学模型。
由于条形码是由黑白相间的且以黑色起头的k块组成,每一块最少含有1条,最多含有m条,k块合起来为n条。因此,一个条形码可以由一个k元组(x1,x2,…,xk)表示,且1≤xi≤m,∑(i=1..k)xi=n。相应地,任意一个满足上述条件的k元组唯一表示一个满足条件的条形码。容易证明,所设的k元组与条形码满足一一对应的关系。满足条件的条形码的个数即为所设的k元组的个数。即方程∑(i=1..k)xi=n,1≤xi≤m的整数解的个数。
最容易想到的是搜索算法1:由于xi的取值范围已经确定,我们可以穷举所有的xi的取值,再检查有多少组解满足∑(i=1..k)xi=n。程序很容易编写,但复杂度却很高,为mk,由于m,k都可能达到30,因此该算法是很低效的。
搜索算法低效的原因是没有很好的利用∑(i=1..k)xi=n这个约束条件,而只将其作为一个判定条件。最容易想到的改进策略是:如要求方程x1+x2+x3=4,1≤x1,x2,x3≤2的整数解的个数。若x1=1,则方程化为x2+x3=4-x1=4-1=3,若x1=2方程化为x2+x3=2。原方程的整数解的个数,正是方程x2+x3=3,x2+x3=2的整数解的个数的和。这样,我们把含3个未知数的方程的整数解个数的问题化为了若干含2个未知数的方程的整数解个数的问题。以此类推,最终可以化为求含一个未知数的方程的整数解个数的问题。容易得出,方程x=n,1≤x≤m的整数解的个数为:当1≤n≤m时,有1个解,否则,有0个解。
容易写出搜索算法2:
Func Count(k,n):LongInt;{∑(i=1..k)xi=n,1≤xi≤m的整数解个数}
begin
if k=1 then if 1≤n≤m then Count:=1
else Count:=0
else for i:=1 to m do Inc(Count,Count(k-1,n-i)){***}
end;
k该程序的时间复杂度仍然为m ,但我们可以通过改进{***}句而将复杂度降低到O(N),
其中N为Count函数的返回值,即方程的整数解个数。具体方法是通过修改循环语句的起始值和终止值:for i:=MinI to MaxI do Inc(Count,Count(k-1,n-i));其中,MinI=Max{1,n-m*(k-1)},MaxI=Min{m,n-k+1}。而方程的整数解个数可以达到6*10甚至更高。显然,这个程序是不高效的。其原因在于所建立的数学模型的抽象程度不高。
我们将方程的整数解个数的模型进一步抽象为:k个在1..m之间的数的和为n的方案数。新的模型与方程的整数解个数的模型似乎没有不同,仅仅是将原模型中未知数xi抽象为k个数。但事实上,这个并不大的变化可以很大程度上的优化程序:我们用F(k,n)表示k个在
1..m之间的数的和为n的方案数,显然有方程式
F(k,n)=∑(i=1..m)F(k-1,n-i)。
和初始条件:若i≠0则F(0,i)=0,F(0,0)=1。
容易写出动态规划程序:
Proc Count;
begin
FillChar (F, Sizeof (F), 0);
F [0,0]: =1;
for i:=1 to n do
for j:=1 to k do
for p:=1 to m do
if i>=p then Inc(F[i,j],F[i-p,j-1])
end;
动态规划的程序的时间复杂度为O(n*k*m)≤30*30*30=27000。
动态规划的特点之一是速度快,另一点便是丰富了运算结果。如本题,我们不仅计算出题设条形码的个数,还计算出了所有由i块组成,每一块最少含有1条,最多含有m条,i块一共为j条的条形码的个数(1≤i≤k,1≤j≤n)。而这些信息可以很方便的解决本题的第二问。
要计算一个条形码的编号,可以先统计在字典顺序中比该条形码小的条形码的个数。这是很容易做到的。具体程序如下:
Func Index (n, k, p: Integer): LongInt;
begin
if k<=1 then Index:=1 else begin
x:=0;
if Odd(p) then begin q:=1;Delta:=1 end else begin q:=m;Delta:=-1 end;
while l[p]<>q do begin
if n>=q then Inc(x,F[n-q,k-1]);
Inc (q,Delta)
end;
8
Inc (x, Index (n-q, k-1, p+1));
Index: =x
end
end;
其中L数组存放的是所读入的条形码的所对应的k元组。如条形码1001110对应的L数组为L[1]=1,L[2]=2,L[3]=3,L[4]=1。该过程的时间复杂度为O(n*k)≤30*30=900
再得到了完美的解答之后,我们再来看看前面的搜索算法。容易看出,搜索算法2可改为:
Func Count(k,n):LongInt;{∑(i=1..k)xi=n,1≤xi≤m的整数解个数}
begin
if F[k,n]=NULL then
if k=0 then F[k,n]:=1
else for i:= Max{1,n-m*(k-1)} to Min{m,n-k+1} do
Inc (F [k, n], Count (k-1, n-i));
Count: =F [k, n]
end;
改进后的算法即为动态规划的递归式写法。此算法可以看作是动态规划算法的改进。因为对决策变量i的初始值和终止值的修正,使得其计算次数较递推写法的动态规划更少。也就是说,我们通过对搜索的算法的改进,得到了同样的动态规划的算法。
那么动态规划与搜索的关系究竟是什么呢,我们再来看另外一个问题:
序关系计数问题 (福建试题)
一、问题描述
用关系‘<’和‘=’将3个数A、B和C依次排列有13种不同的关系:
A<B<C, A<B=C, A<C<B, A=B<C, A=B=C, A=C<B,
B<A<C, B<A=C, B<C<A, B=C<A,
C<A<B, C<A=B, C<B<A。
编程求出N个数依序排列时有多少种关系。
二、分析
<1>.枚举出所有的序关系表达式
我们可以采用回溯法枚举出所有的序关系表达式。N个数的序关系表达式,是通过N个大写字母和连接各字母的N-1个关系符号构成。依次枚举每个位置上的大写字母和关系符号,直到确定一个序关系表达式为止。
由于类似于‘A=B’和‘B=A’的序关系表达式是等价的,为此,规定等号前面的大写字母在ASCII表中的序号,必须比等号后面的字母序号小。基于这个思想,我们很容易写出解这道题目的回溯算法。
算法1,计算N个数的序关系数。
procedure Count(Step,First,Can);
{Step表示当前确定第Step个大写字母;
First表示当前大写字母可能取到的最小值;
Can是一个集合,集合中的元素是还可以使用的大写字母}
begin
if Step=N then begin{确定最后一个字母}
for i:=First to N do if i in Can then Inc(Total);{Total为统计的结果}
Exit
end;
for i:=First to N do{枚举当前的大写字母}
if i in Can then begin{i可以使用}
Count(Step+1,i+1,Can-[i]);{添等号}
Count(Step+1,1,Can-[i]){添小于号}
end
end;
调用Count(1,1,[1..N])后,Total的值就是结果。该算法的时间复杂度是O(N!)
图4-8 N=3时的解答树
<2>.粗略利用信息,优化算法1
算法1中存在大量冗余运算。如图4-8,三个方框内子树的形态是完全一样的。一旦我们知道了其中某一个方框内所产生的序关系数,就可以利用这个信息,直接得到另两个方框内将要产生的序关系数。
显然,在枚举的过程中,若已经确定了前k个数,并且下一个关系符号是小于号,这时所能产生的序关系数就是剩下的N-k个数所能产生的序关系数。
设i个数共有F[i]种不同的序关系,那么,由上面的讨论可知,在算法1中,调用一次Count(Step+1,1,Can-[i])之后,Total的增量应该是F[N-Step]。这个值可以在第一次调用Count(Step+1,1,Can-[i])时求出。而一旦知道了F[N-Step]的值,就可以用Total:=Total+F[N-Step] 代替调用Count(Step+1,1,Can-[i])。这样,我们可以得到改进后的算法1-2。
算法2,计算N个数的序关系数。
procedure Count(Step,First,Can);
{Step,First,Can的含义同算法1}
begin
if Step=N then begin{确定最后一个字母}
for i:=First to N do if i in Can then Inc(Total);{Total为统计的结果}
Exit
end;
for i:=First to N do{枚举当前的大写字母
}