【问题标题】:How do I count numbers that contain one digit, but not another?如何计算包含一个数字但不包含另一个数字的数字?
【发布时间】:2021-09-12 04:37:08
【问题描述】:

我最近遇到了一个面试问题,虽然有一个显而易见的解决方案,但我很难找到一个更有效的解决方案。

实际问题涉及计数从ab(最多2^64)的数字,满足数字68,但不能同时满足两者。他们称之为“幸运数字”。比如:

126 - lucky
88 - lucky
856 - not lucky

显而易见的想法是通过测试ab 之间的每个数字作为字符串来强制它,以检查相关字符。但是,这与预期的一样慢。

我尝试了一个更好的解决方案,首先计算所有“幸运数字”,其位数介于 ab 的位数之间(通过计算可能的组合):

long n = 0;

for (int occurrences = 1; occurrences <= maxDigits; occurrences++) {

    n += (long) Math.pow(8, digits - occurrences) * choose(digits, occurrences);
}

return 2 * n;

然后使用蛮力方法计算我数过的额外幸运数字的数量。例如,如果a = 3b = 21,我可以计算12 数字幸运数字的数量,然后减去[1, 3)(21, 99] 中的数字。

但是,尽管这是一个巨大的改进,但在大多数情况下,蛮力元素仍然会大大减慢它的速度。

我觉得我一定缺少了什么,因为其余的面试问题都比较简单。有人知道更好的解决方案吗?


尽管我在 Java 中标记了这个问题,但同样感谢任何其他语言或伪代码的帮助。

【问题讨论】:

  • 我不知道解决方案,但是,我不会测试区间内的每个数字,而是生成从 a 到 b 的所有可能的幸运数字并计算它们(实际上,没有必要存储每个幸运数字数字,只需增加一个计数器就可以了)
  • 您可以采用相反的方式将数字生成为字符串,然后解析它们以获取整数值。解析效率不高,但它会生成要检查的候选者少得多。或者只生成标记值(例如“0...08”和“9...98”)并计算包含多少个值(例如“xxx8”具有“xxx”数字的“999”值)和加到总数中,上限和下限的特殊情况(例如减号lower 和减号(999-upper))。抱歉没有时间得出一个实际的答案,希望这足以给出一个方向。
  • 一个重要的优化:你的组合和实际上是9**n - 8**n
  • @fps 我也是这么想的,但我没有找到这样做的“模式”……是吗?
  • @fps 是的,我想。我希望会有一个我没有想到的技巧......谢谢你回到这个。

标签: java algorithm optimization


【解决方案1】:

我会说你在正确的轨道上。直觉是分别处理ab 更容易。制作函数count_lucky_numbers_below(n) 允许

return count_lucky_numbers_below(b) - count_lucky_numbers_below(a);

组合方法绝对是一种方法(请记住,总和实际上等于9**n - 8**n,无需计算二项式系数)。

最后一个技巧是向下递归多个数字。

假设n 是一个N-digit 数字,最高有效数字是5。以较小数字开头的每组N-digit 数字对总数贡献S = 9**(N-1) - 8**(N-1);您立即拥有5*S 的幸运数字。要处理余数,您需要计算N-1-digit 尾部的幸运数字。

当然,如果最高有效位在5以上,则必须小心。您需要特殊情况下为6或8,但似乎并不太复杂。

【讨论】:

  • 我可以请您为此答案发布一个示例吗?我认为您是对的,但是从 A 到 Z 的示例将使这成为更好的资源。谢谢!
  • 我设法使用您的逻辑找到了一个非常快速的解决方案,但是它仍然没有我希望的那么简洁。也许我错过了一个简化,所以如果这不是你的想法,请告诉我。无论如何,感谢您将我推向正确的方向(或至少是某种方向!)。
  • @Eugene 希望我的回答能提供上述资源(如果这是 this 回答的意图)
【解决方案2】:

最后,@user58697 的回答将我推向了寻找解决方案的正确方向。使用我的(尽管非常原始)基准测试,它可以在不到 2 纳秒的时间内处理 12^63 - 1,所以它绝对足够快。然而,它仍然比我想要的更冗长,特别是考虑到我最初预计会在半小时内编写它,所以我觉得仍然有一个更简单的解决方案可以提供相当的性能。

long countLuckyNumbersBetween(long a, long b) {

    return countLuckyNumbersBelow(b) - countLuckyNumbersBelow(a - 1);
}

long countLuckyNumbersBelow(long n) {

    return countNumbers(n, 6, 8) + countNumbers(n, 8, 6);
}

/**
 * Counts the natural numbers in [0, {to}] that have {including} as a digit, but not {excluding}.
 * {excluding} should be in (0, 9] or -1 to exclude no digit.
 */
long countNumbers(long to, int including, int excluding) {

    if (including == -1) return 0;

    if (to < 10) {

        if (to >= including) {

            return 1;
        } else {

            return 0;
        }
    }

    int nSignificand = significand(to);
    int nDigits = countDigits(to);

    long nTail = to % (long) Math.pow(10, nDigits - 1);

    // The count of numbers in [0, 10^(nDigits-1)) that include and exclude the relevant digits
    long bodyCount;
    if (excluding == -1) {

        bodyCount = (long) (Math.pow(10, nDigits - 1) - Math.pow(9, nDigits - 1));
    } else {

        bodyCount = (long) (Math.pow(9, nDigits - 1) - Math.pow(8, nDigits - 1));
    }

    long count = 0;

    for (int i = 0; i < nSignificand; i++) {

        if (i == including) {

            if (excluding == -1) {

                count += Math.pow(10, nDigits - 1);
            } else {

                count += Math.pow(9, nDigits - 1);
            }
        } else if (i != excluding) {

            count += bodyCount;
        }
    }

    if (nSignificand == including) {

        count += 1 + nTail - countNumbers(nTail, excluding, -1);
    } else if (nSignificand != excluding) {

        count += countNumbers(nTail, including, excluding);
    }

    return count;
}

int significand(long n) {

    while (n > 9) n /= 10;
    return (int) n;
}

int countDigits(long n) {

    if (n <= 1) {

        return 1;
    } else {

        return (int) (Math.log10(n) + 1);
    }
}

【讨论】:

    【解决方案3】:

    这是另一种方法:

    264 = 18446744073709551616

    我们可以将数字表示为分量的总和(每个数字位置一个分量):

      18446744073709551616              associated range of numbers
      ————————————————————      ———————————————————————————————————————————
      0xxxxxxxxxxxxxxxxxxx  =>  [00000000000000000000;09999999999999999999]
      17xxxxxxxxxxxxxxxxxx  =>  [10000000000000000000;17999999999999999999]
      183xxxxxxxxxxxxxxxxx  =>  [18000000000000000000;18399999999999999999]
      1843xxxxxxxxxxxxxxxx  =>  [18400000000000000000;18439999999999999999]
      18445xxxxxxxxxxxxxxx  =>  [18440000000000000000;18445999999999999999]
      ...
      1844674407370955160x  =>  [18446744073709551600;18446744073709551609]
      18446744073709551616  =>  [18446744073709551610;18446744073709551616]
    

    如果我们可以计算每个组件的幸运数字的数量,那么每个组件的数量之和将是 264 的总数。

    请注意,每个组件都包含一个前缀,后跟xs。
    想象一下,我们知道一个 n 位数 xx..x 中有多少个幸运数字(即数字 [0..0 - 9..9]),我们称之为 N(n)

    现在让我们看一个组件18445x..x。其中18445 是一个前缀和一个n 位数xx..x
    在这个组件中,我们查看从 18440xx..x18445xx..x 的所有数字。
    对于每个项目1844dxx..x,我们查看前缀1844d

    • 如果前缀不包含68,则与不带前缀的x..x相同=> N(n)特殊数字
    • 如果前缀包含6而没有8,那么x..x不能包含8 => 9ⁿ特殊数字
    • 如果前缀包含8而没有6,那么x..x不能包含6 => 9ⁿ特殊数字
    • 如果前缀包含68 => 0 特殊数字

    现在让我们计算 N(n) — n 位 xx..x(即 [0..0 - 9..9])中幸运数字的数量。
    我们可以迭代地做:

    1. n=1:只有 2 个可能的数字:86 => N(1)=2

    2. n=2:有2组:

      • 8 存在:8xx8 其中x 是除6 之外的任何数字
      • 6 存在:6xx6 其中x 是除8 之外的任何数字

      => N(2)=4*9=34.

    3. n=3: 让我们修复第一个数字:

      • 0xx5xx, 7xx, 9xx => 8 * N(2)
      • 6xx: xx 是除 8 之外的任意 2 位数字 =>
      • 8xx: xx 是除 6 之外的任意 2 位数字 => => N(3) = 8*N(2) + 2*9².
    4. n=k+1 => N(k+1) = 7*N(k) + 2*9ᵏ

    这是一个实现(未 100% 测试):

    public final class Numbers {
    
      public long countLuckyNumbersBelow(BigInteger num) {
        if (num.compareTo(BigInteger.ZERO) < 0) {
          throw new IllegalArgumentException("num < 0: " + num);
        }
        var numberText = num.toString();
        var result = 0L;
        for (var digitPosition = 0; digitPosition < numberText.length(); digitPosition++) {
          result += countLuckyNumbersForComponent(numberText, digitPosition);
        }
        return result;
      }
    
      private long countLuckyNumbersForComponent(String numberText, int digitPosition) {
        var prefixEndIdx = numberText.length() - 1 - digitPosition;
        var prefixHas6s = containsChar(numberText, '6', prefixEndIdx);
        var prefixHas8s = containsChar(numberText, '8', prefixEndIdx);
        if (prefixHas6s && prefixHas8s) {
          return 0;
        }
        var result = 0L;
        for (var c = numberText.charAt(prefixEndIdx) - 1; c >= '0'; c--) {
          var compNo6s = (!prefixHas6s) && (c != '6');
          var compNo8s = (!prefixHas8s) && (c != '8');
          if (compNo6s && compNo8s) {
            result += countLuckyNumbers(digitPosition);
          } else if (compNo6s || compNo8s) {
            result += power9(digitPosition);
          }
        }
        return result;
      }
    
      private static boolean containsChar(String text, char c, int endIdx) {
        var idx = text.indexOf(c);
        return (idx > 0) && (idx < endIdx);
      }
    
      private long[] countLuckyNumbersCache = {0L, 0L};
    
      /**
       * Computes how many lucky numbers are in an n-digit `xx..x`
       */
      private long countLuckyNumbers(int numDigits) {
        if (countLuckyNumbersCache[0] == numDigits) {
          return countLuckyNumbersCache[1];
        }
        long N;
        if (numDigits <= 1) {
          N = (numDigits == 1) ? 2 : 0;
        } else {
          var prevN = countLuckyNumbers(numDigits - 1);
          N = (8 * prevN) + (2 * power9(numDigits-1));
        }
        countLuckyNumbersCache[0] = numDigits;
        countLuckyNumbersCache[1] = N;
        return N;
      }
    
      private long[] power9Cache = {0L, 1L};
    
      /**
       * Computes 9<sup>power</sup>
       */
      private long power9(int power) {
        if (power9Cache[0] == power) {
          return power9Cache[1];
        }
        long res = 1;
        var p = power;
        if (power > power9Cache[0]) {
          p -= power9Cache[0];
          res = power9Cache[1];
        }
        for (; p > 0; p--) {
          res *= 9;
        }
        power9Cache[0] = power;
        power9Cache[1] = res;
        return res;
      }
    }
    

    顺便说一句,我花了半天时间,我不知道怎么可能在 30 分钟内完成。
    我猜你的面试官希望你向他们展示你的思维过程。

    【讨论】:

      【解决方案4】:

      这是我尝试的结果。

      首先,让我解释一下我使用的逻辑。

      我使用公式S = 9<sup>N</sup> — 8<sup>N</sup>(在the user58697's answer中提到)来计算有多少N位数字是幸运的。
      如何得到这个公式:

      • 对于 N 位数字,总共有 10<sup>N</sup> 个数字:N 个数字,每个数字可以取 10 个值之一:[0-9]
      • 如果我们只计算没有6 的数字,那么每个数字只能取9 个值中的一个[0-5,7-9] — 总共是9<sup>N</sup> 数字
      • 现在我们也只需要带有8 的数字。
        我们可以很容易地计算出有多少个数字同时具有68:这些数字中的数字只能取8 个值中的一个[0-5,7,9] — 它是8<sup>N</sup> 总数。
        因此,有S = 9<sup>N</sup> — 8<sup>N</sup> 号码有8 而没有6

      对于带6 和不带8 的数字,公式相同。
      此外,没有6 的数字不会与没有8 的数字相交——所以我们可以将它们相加。

      最后,由于我们知道如何计算区间 [0;10<sup>N</sup>] 的幸运数字,我们需要将区间 [0; our arbitrary number] 拆分为合适的子区间。
      例如,我们可以这样拆分号码9845637

      Sub-interval Prefix Digit N-digit interval
      0000000 - 8999999 0 - 8 000000 - 999999
      9000000 - 9799999 9 0 - 7 00000 - 99999
      9800000 - 9839999 98 0 - 3 0000 - 9999
      9840000 - 9844999 984 0 - 4 000 - 999
      9845000 - 9845599 9845 0 - 5 00 - 99
      9845600 - 9845629 98456 0 - 2 0 - 9
      9845630 - 9845637

      现在我们可以计算每个子区间的数字(只需注意前缀中的数字 - 它们可能包含 86),然后将这些数字相加得到最终结果。

      代码如下:

        // Special value for 'requiredDigit': no required digit
        private static char NIL = Character.MAX_VALUE;
      
        public static long countLuckyNumbersUpTo(BigInteger number) {
          if (number.compareTo(BigInteger.ZERO) < 0) {
            throw new IllegalArgumentException("number < 0: " + number);
          }
          var numberAsDigits = number.toString();
          return countNumbersUpTo(numberAsDigits, '6', '8') + countNumbersUpTo(numberAsDigits, '8', '6');
        }
      
        // count all numbers in [0;'numberAsDigits'] which have 'requiredDigit' and no 'excludeDigit'
        private static long countNumbersUpTo(String numberAsDigits, char requiredDigit, char excludeDigit) {
          var highDigit = numberAsDigits.charAt(0);
      
          if (numberAsDigits.length() == 1) {
            return (requiredDigit != NIL)
                ? ((highDigit >= requiredDigit) ? 1 : 0)
                : numDigitsInInterval('0', highDigit, excludeDigit);
          }
      
          var tailDigits = numberAsDigits.substring(1);
          var result = 0L;
      
          // numbers where the highest digit is in [0;`highDigit`)
          var numGoodDigits = numDigitsInInterval('0', (char) (highDigit - 1), excludeDigit);
          var containsRequiredDigit = (requiredDigit != NIL) && (highDigit > requiredDigit);
          if (containsRequiredDigit) {
            result += totalNumbers(tailDigits.length(), NIL);
            numGoodDigits--;
          }
          if (numGoodDigits > 0) {
            result += numGoodDigits * totalNumbers(tailDigits.length(), requiredDigit);
          }
      
          // remaining numbers where the highest digit is `highDigit`
          if (highDigit != excludeDigit) {
            var newRequiredDigit = (highDigit == requiredDigit) ? NIL : requiredDigit;
            result += countNumbersUpTo(tailDigits, newRequiredDigit, excludeDigit);
          }
      
          return result;
        }
      
        private static int numDigitsInInterval(char firstDigit, char lastDigit, char excludeDigit) {
          var totalDigits = lastDigit - firstDigit + 1;
          return (excludeDigit <= lastDigit) ? (totalDigits - 1) : totalDigits;
        }
      
        // total numbers with given requiredDigit in [0;10^numDigits)
        private static long totalNumbers(int numDigits, char requiredDigit) {
          return (requiredDigit == NIL) ? pow(9, numDigits) : (pow(9, numDigits) - pow(8, numDigits));
        }
      
        private static long pow(int base, int exponent) {
          return BigInteger.valueOf(base).pow(exponent).longValueExact();
        }
      

      【讨论】:

        猜你喜欢
        • 2011-06-01
        • 1970-01-01
        • 2012-11-21
        • 1970-01-01
        • 1970-01-01
        • 2023-01-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多