动态规划:计算两个数之间的数字数量

3
给定两个数字 X 和 Y,它们之间有多少个数字包括至少一半的数字相同?例如,1122和4444可以,而11234和112233不行。
显然,最直接的方法是从X开始增加1,一直到Y,然后检查每个数字,但这太慢了,因为X和Y的范围在100到10^18之间。我知道这是某种形式的动态规划,并且应该使用字符串来表示数字,但我无法进一步得到解决方案。
欢迎提供任何帮助。谢谢!

1231 是否符合条件?你的例子不具有代表性。 - AnT stands with Russia
1
是的,它确实可以:有两个1和4个数字,2> = 4/2,所以是的。 - user9008839
@NL628 我详细解释了算法。你哪一部分不清楚? - Alireza
@Alireza 哈哈,你真的想要赏金 :P - NL628
1
@NL628 哈哈,不,我已经为这个问题得到了赏金。我只想添加更多细节,如果你没有理解清楚的话。 - Alireza
@Alireza 哈哈,我之前在 USACO 比赛中见过这个问题(它被称为里程表),我读了他们的解决方案,他们只在 dp 解决方案中使用了 4 个维度,而你使用了 6 个,所以我想知道你的解决方案使用更多维度的原因是什么...是否有使用 6 个维度的优势? - NL628
4个回答

7

我将为您分步解释:

第一步:

要解决X和Y之间的范围问题,通常可以先从0到X和0到Y-1进行计数,然后相减得到结果。例如,如果您有一个像f(N)这样的函数,用于计算0到N之间至少有一半数字相同的数字数量,则最终结果如下:

f(X) - f(Y-1)
第二步: 接下来我们要计算 f(N)。我们把这个函数分成两个子函数,一个用于计算与 N 有相同位数的数字的结果(我们称之为 f_equal),另一个用于计算比 N 小的数字中符合条件的数的数量(我们称之为 f_less)。例如,如果 N 是 19354,我们计算 0 到 9999 之间符合条件的数字,然后在另一个方法中计算 10000 到 19354 之间的收藏数字,之后将结果相加。接下来,我将向您介绍如何实现这两种方法。 第三步: 在这里,我们要计算 f_less 方法。您可以通过一些数学方法来完成它,但我总是更喜欢编写一个简单的 DP 来解决这些任务。我将编写递归函数,您可以使用记忆化或使用一些循环来进行自底向上的计算(这留作练习给您)。
long long f_less(int curDigit, int favNum, int favNumCountSoFar, int nonFavNum, int nonFavNumCountSoFar, int maxDigit){
    if(curDigit == maxDigit ){
        //for numbers with even maxDigit there may be a case when we have 2 favorite numbers
        //and we should count them only once. like 522552
        if(favNumCountSoFar*2 == maxDigit && favNumCountSoFar == nonFavNumCountSoFar) return 1;
        if(2*favNumCountSoFar >= maxDigit) return 2;
        return 0;
    }
    long long res = 0;
    for(int i=(curDigit==0?1:0);i<=9;++i) //skip the leading zero
        if(i==favNum)
            res += f_less(curDigit+1, favNum, favNumCountSoFar + 1, nonFavNum, nonFavNumCountSoFar,maxDigit);
        else
            res += f_less(curDigit+1, favNum, favNumCountSoFar, i, (i==nonFavNum?nonFavNumCountSoFar+1:1),maxDigit);
    return res;
}

并对0到9的所有数字进行调用:

long long res = 0;
for(int maxDigit = 1; maxDigit < NUMBER_OF_DIGITS(N); ++maxDigit)
    for(int favNumber = 0; favNumber < 10; ++favNumber)
        res += f_less(0, favNumber, 0, -1, 0, maxDigit);

第四步:

最后,我们需要计算 f_equal。在递归函数中,我们需要将数字保留为字符串,以便始终检查是否仍在小于 N 的范围内。以下是 f_equal 的实现(可以再次使用记忆化或进行自底向上的处理):

string s = NUM_TO_STRING(N);
int maxDigit = s.size();
long long f_equal(int curDigit, int favNum, int favNumCountSoFar,int nonFavNum, int nonFavNumCountSoFar, bool isEqual){ //isEqual checks that whether our number is equal to N or it's lesser than it 
    if(curDigit == maxDigit ){
        //for numbers with even maxDigit there may be a case when we have 2 favorite numbers
        //and we should count them only once. like 522552
        if(favNumCountSoFar*2 == maxDigit && favNumCountSoFar == nonFavNumCountSoFar) return 1;
        if(2*favNumCountSoFar >= maxDigit) return 2;
        return 0;
    }
    long long res = 0;
    for(int i=(curDigit==0?1:0);i<=9;++i){ //skip the leading zero
        if(isEqual && i>(s[curDigit]-'0')) break;
        if(i==favNum)
            res += f_equal(curDigit+1, favNum, favNumCountSoFar + 1, nonFavNum, nonFavNumCountSoFar, isEqual && (i==(s[curDigit]-'0')));
        else
            res += f_equal(curDigit+1, favNum, favNumCountSoFar, i, (i==nonFavNum?nonFavNumCountSoFar+1:1), isEqual && (i==(s[curDigit]-'0')));
    } 
    return res;
}

并将其称为:

long long res = 0;
for(int favNumber = 0; favNumber < 10; ++favNumber)
    res += f_equal(0, favNumber,0, -1, 0, true);

最终结果是res/2。代码已经测试并且运行良好。

你的时间和空间复杂度是多少? - Pham Trung
@PhamTrung 在使用记忆化或自底向上的方法后,您需要一个大小为[n][m][n][m][n][n]的数组,其中n是最大数字长度,即18,m是1位数字的数量,即10。因此,空间和时间复杂度为O(n^4 m^2)。这大约是1000万次迭代,在典型家用电脑上运行不到1秒钟。 - Alireza

4
显然,你不会通过考虑范围内的所有数字来实现这一点。相反,考虑以生成你想要的数字为基础。例如,设计一个函数,给定位数不超过n,将生成所有符合条件的数字。
例如,对于5位数,你需要至少有三个1或三个2等数字。你能一次性做到吗?还是你需要将恰好有三个1的数字与那些有更多1的数字分开?
既然你已经思考了这个问题,那么思考一下:不要生成所有这些数字,只需计算它们的数量。例如,对于三个1和两个其他数字,你有9*9对其他数字(确保不重复计算诸如11122之类的东西)。你可以用10种方式排列1,并交换其他两个数字。
请注意,在有偶数位数字的情况下,问题有些不同:你必须避免重复计算一半一半的数字,例如111222。
这可以帮助你开始动手吗?
回答评论03 Dec:
@bobjoe628: 这并不是一个完整的算法,而是一个让你开始的建议。是的,你有几个组合问题要处理。至于11122233,我不确定你的担忧是什么:与任何这样的排列问题一样,你必须处理每个数字可互换的情况。有10C5种分配1的方法;在剩余的位置上,有5C3种分配2的方法;另外两个位置是3'3。现成的算法(即浏览器搜索)将涵盖这些计算。
我相信你能编写一个生成数字的算法:请注意,你只需要一组数字,因此可以安全地按升序生成数字,就像你给出的例子一样:1111122233。一旦你生成了它,你的组合代码应该涵盖所有唯一的排列组合。
最后,请注意,大多数语言都有支持包,可以为你执行排列和组合。

1
怎么防止自己计算带有前导零的数字? - NL628
1
是的...我也是这么想的。另外,1111122233会发生什么? - user9008839
1
@bobjoe628,你说得对。另外,如果X = 15286,那么你如何防止获得小于该值的5位数? - NL628
@Prune在C++中有哪些包可以执行组合操作?我只能包含标准库,并且我不知道任何执行组合操作的函数,尽管我知道排列的函数... - NL628

1
这里是部分组合解答。我省略了如何使用该函数构建完整答案的步骤。
(请参见此处,其中包含更详细的注释:https://repl.it/@gl_dbrqn/AllSteelblueJuliabutterfly
修复数字中最左边的位数 L,并且该数字有 R 个位于 L 右侧的数字,我们可以通过以下方式计算出如何分配 d 数字的 (N / 2) 或更多个:
Python 代码
import math, operator, collections   

# Assumes L has at least one digit set
# f({'string':'12', 'digit_frequencies':[0,1,1,0,0,0,0,0,0,0], 'num_digit_frequencies': 2}, 6)
def f(L, N):
  R = N - len(L['string'])

  count = 0
  counted = False

  for digit_frequency in L['digit_frequencies']:       
    start = int(math.ceil(N / 2.0)) - digit_frequency
    if start > R:
      continue

    for i in xrange(start, R + 1):
      if not (N & 1) and not counted:
        if L['num_digit_frequencies'] == 1 and not digit_frequency and i == N / 2:
          count = count - choose(R, i)

        if L['num_digit_frequencies'] == 2 and digit_frequency and not any([x > N / 2 for x in L['digit_frequencies']]) and i + digit_frequency == N / 2:
          count = count - choose(R, i)
          counted = True

      m = 9**(R - i)
      n = R - i + 1
      k = i

      count = count + m * choose(n + k - 1, k)

  return count


# A brute-force function to confirm results 
# check('12', 6)
def check(prefix, length):
  result = [x for x in xrange(10**(length - 1), 10**length) if len(str(x)) == length and str(x).startswith(prefix) and isValid(str(x))]

  print result
  return len(result)

def isValid(str):
  letters = collections.Counter(str)
  return any([x >= math.ceil(len(str) / 2.0) for x in letters.values()])


# https://dev59.com/W2445IYBdhLWcg3wR4VO
def choose(n, r):
  r = min(r, n-r)
  if r == 0: return 1
  numer = reduce(operator.mul, xrange(n, n-r, -1))
  denom = reduce(operator.mul, xrange(1, r+1))
  return numer//denom

1
数字0只是简写。实际上,有无限多个前导零和无限多个尾随零(小数点后),如...000000.000000...
对于所有整数而言,显然小数点后至少有与小数点前非零数字一样多的0;因此所有整数都可以计算。
0和1之间有无限多个数字;所有这些数字左侧至少有与小数点后非零数字一样多的0。同样适用于0和-1之间的数字。
对于计算机可以存储的几乎所有浮点数,没有足够的位数来取消所有前导和尾随零。
唯一无法计数的数字是正负无穷大以及一些但并非所有小于等于1或大于等于-1的无理数。
代码:
float getCount(int x, int y) {
    if(x == y) return 0.0; // Special case (no numbers are between x and y)
    return INFINITY;       // The closest value to the correct answer that a computer can use
}

@NL628:你说得对 - 我没有想到在我的代码中使用动态规划的方法。 ;) - Brendan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接