计算 n 位字符串的数量,使得在字符串的每个前缀中,零的数量至少是一的数量的 k 倍

Posted

技术标签:

【中文标题】计算 n 位字符串的数量,使得在字符串的每个前缀中,零的数量至少是一的数量的 k 倍【英文标题】:Count the number of n-bit strings such that, in each prefix of the string, the number of zeroes is at least k times the number of ones 【发布时间】:2020-01-11 06:33:13 【问题描述】:

我们如何?

例子:

(1) 在 n = 5 和 k = 3 的情况下,有 3 个这样的字符串:00000、00001 和 00010。在每个字符串中,每个初始子字符串的零至少是一的三倍。

(2) 当 n = 6 和 k = 2 时,有 8 个这样的字符串:000000、000001、000010、000011、000100、000101、001000 和 001001。

我实现了递归计数,但我需要一个比这更有效的算法或公式。如果有人能在输出中找到一种模式以避免浪费在递归上的时间,那就太好了。

为了澄清问题,我将说明一些限制条件, 1

对于任何想查看代码的人,这是我实现的递归代码-

#include <iostream>
using namespace std;

int count;
int k;

// c0 is no. of 0s and c1 is no. of 1s
void rec(int c0, int c1, int n)

    if(n>=0)
    
        if(!n)
        
            count++;
        

        rec(c0+1,c1,n-1);

        if((c1+1)*k<=c0)
        
            rec(c0,c1+1,n-1);
        
    


int main()

    int n;

    cin>>n>>k;
    rec(k,0,n-k);
    cout<<count;

    return 0;

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 我将原始标题和文本中的“x”更改为“k”,以便更清楚地说明它是问题的一个参数,避免任何人将它与待解决的未知数混淆。保持符号一致会很好——在后续编辑中使用 k 或将所有文本和代码更改回使用 x。 任何不一致的字符串都必须有一个正好有 floor(m/(k+1))+1 个前缀的前缀,其中 m 是前缀的长度。我想知道一些用于计算此类字符串的简单组合和使用包含 - 排除原则是否可以提供比递归更好的求和。 @eric:也许吧,但是有一个简单的 DP 解决方案,它在二次时间和线性空间中运行,并且在内循环中只使用一个加法。 @rici:你能解释一下 DP 解决方案吗? 【参考方案1】:

虽然可能存在涉及组合分析的更复杂的解决方案,但生成一个在 O(n²) 时间内工作并需要 O(n) 额外空间的动态规划解决方案非常简单。由于该解决方案的内部循环仅涉及一次加法,因此在 n 变大之前它的速度非常快,尽管您确实需要多精度算术来解决除最小问题之外的所有问题。

动态编程涉及颠覆递归;在动态规划中,我们不是从目标问题开始并递归地将其分解为更简单的问题,而是以适当的顺序解决所有更简单的问题,以便在需要它们之前获得中间结果。当相同的简单问题在递归过程中多次出现时,它是有效的,这与记忆递归的用例相同。但是,DP 可能比记忆更有效,因为中间计算的正确排序通常意味着在任何给定时间需要记住值的中间计算的数量显着减少。 (它还避免了检查所需的中间值是否已经计算过的开销,尽管这通常并不重要。)

在这种情况下,我们将使用一个非常简单的递归,它基于以下事实:任何长度为m 的有效二进制序列都是长度为m-1 后跟01。 (并非总是可以将1 放在有效二进制序列的末尾,因为这可能违反计数约束。但始终可以删除恰好位于有效二进制序列末尾的1二进制序列。)

我们实际上要计算的是长度为n 的有效序列的数量与正好为m 的序列的数量。然后将m的不同值的所有计数相加,就可以得到长度为n的有效序列数。我们可以区分三种情况:

    m 太大;有这么多,不可能构造一个序列,其中一个的数量至少是k 乘以零的数量。很容易检查这种情况;零的数量是n - m(因为所有不是一的东西都必须是零),所以如果m &gt; k(n - m)m 太大了。稍加代数就会知道这和m &gt; n / (k + 1)是一样的。

    m 为零。只有一个长度为n 的有效序列没有1:n 的序列为零。无论k 的值如何,该序列都是有效的。

    1234563必须是长度为n-1m-1 的有效序列,后跟另一个,或者是长度为n-1 的有效序列,m 后跟一个零。

将所有这些放在一起,我们可以递归计算:

如果 m = 0,则计数 [n, m] = 1 = Count[n-1, m-1] + Count[n-1, m] 如果 0 n / (k + 1)

现在,让我们将该递归转换为动态编程解决方案。

很明显,给定n 的所有Count[n, ...] 值仅取决于Count[n-1, ...] 的值。因此,如果我们计算Count[n-1,...] 的所有值,我们可以使用这些值生成Count[n, ...] 值的向量,之后我们不再需要记住Count[n-1, ...] 的任何值。所以我们显然只需要保留两个长度为1 + (n / (k + 1)) 的向量。但我们可以做得更好,因为Count[n, m] 的值仅取决于Count[n-1, m]Count[n-1, m-1]。这个事实让我们可以使用单个向量进行计算,前提是我们从 m 的最大有效值开始向后工作。

因此,计算长度为n 的有效位序列的最终解决方案,其中每个前缀至少有k 的每个零:

    创建一个长度为 1 + (n / (k + 1)) 的向量 Count。将 Count[0] 设置为 1,每个其他元素设置为 0。

    对于从 1 到 n(含)的每个 i

    对于从 i / (k + 1) 到 1(含)的每个 j

    Count[j-1] 添加到Count[j]

    返回Count中所有值的总和

【讨论】:

【参考方案2】:
#include <iostream>
using namespace std;

int main()

    int t,i,j,m,n,k,sum;
    
    sum=0;
    
    cin>>n>>k;
    m=n/(k+1);
    
    int count[m+1]=0;
    count[0]=1;
    
    for(i=k+1;i<=n;i++)
    
        for(j=i/(k+1);j>=1;j--)
        
            count[j]=count[j]+count[j-1];
        
    
    
    for(i=0;i<m+1;i++)
    
        sum=sum+count[i];
    
    
    cout<<sum<<endl;
    
    return 0;

使用上面给出的建议,我认为这就是您正在寻找的。​​p>

【讨论】:

【参考方案3】:

通过构造而不是反复试验来查找合法字符串。

从左边开始对字符串位置进行编号,从 1 开始。为方便起见,设j = k+1。第一个1 不能早于位置j;第二个不早于位置2j,以此类推。

例如,对于 (6, 2) 的情况,j=3。第一个1 可以出现在位置3 的任何位置;第二个只能出现在位置 6。

编写一个 DP 函数来完成这个决策过程。暂时忘记生成字符串,我们只需要其中每个元素a[i] 不小于i*j 的所有数组。对于上述情况,第一个决策点是针对初始元素的。选择包括

[]
[3]
[4]
[5]
[6]

第一个和最后一个是终端搜索;其他的被扩展为可能的第二个元素,产生

[3, 6]
[4, 6]
[5, 6]

...这些很容易转换为八种所需的解决方案。

你能从那里实现递归吗?

【讨论】:

【参考方案4】:
    // Trivial recursive solution
    // It appears that one on the recursive calls
    // can be removed as tail recursion
    // Appending multiple '0's, just until another '1' is possible,
    // seems to be a nice optimisation, but will complicate the
    // final logic (at the end-of string) and will need a few
    // extra conditions.

#include <stdio.h>

char buff[100];

unsigned recurse(unsigned zeros, unsigned ones, unsigned x, unsigned len)

unsigned count=0;
unsigned pos = zeros+ones;

if (pos == len) 
        buff[pos] =0;
        printf("%s\n", buff);
        return 1;
        

buff[pos] = '0';
count += recurse(zeros+1, ones, x, len);
        // if no '1' is possible yet,
        // we could loop here, adding more '0' bits
        // , but we must take care not to exceed len
        // and to maintain the count correctly
        // (and possibly yield the resulting string)

if (ones < zeros /x) 
        buff[pos] = '1';
        count += recurse(zeros, ones+1, x, len);
        

return count; // number of solutions, given the prefix [0..pos]


int main(int argc, char **argv)

unsigned x,n, result;
sscanf(argv[1], "%u", &n);
sscanf(argv[2], "%u", &x);

result = recurse(0,0, x, n);
printf("Result=%u\n", result);
return 0;

【讨论】:

以上是关于计算 n 位字符串的数量,使得在字符串的每个前缀中,零的数量至少是一的数量的 k 倍的主要内容,如果未能解决你的问题,请参考以下文章

leetcode-最长公共前缀

最优前缀编码

UVa 11488 超级前缀集合(Trie的应用)

pandas使用zfill函数向dataframe特定数据列的每个字符串添加前置(前缀)补齐字符使得当前数据列内容所有字符长度相同(向列A的每个字符串添加前导零,直到达到4的宽度)

(学习11)哈夫曼算法

(学习11)哈夫曼算法