【问题标题】:Unique string with at least two differences具有至少两个差异的唯一字符串
【发布时间】:2017-04-11 14:29:05
【问题描述】:

我正在寻找一种方法来生成唯一的随机(随机外观)字母数字字符串,并限制每个此类字符串在非相邻位置至少有两个不同的字符。字符串的长度应支持至少 2000 万个唯一值(7 个字符应该绰绰有余)。

例子:

AAAAAAA <- first string
AAAAABB <- does not work (different, but adjacent)
ABAAAAA <- does not work (only one different)
AABAABA <- that works perfectly

我首先考虑使用一些标准函数(我目前使用的是 PostgreSql,但我也可以使用 Oracle)。我可以使用md5() 之类的东西生成随机字符串,但我不知道如何满足其他约束。一个想法是使用levenshtein 来检查每个新生成的字符串与所有已经生成的字符串,并仅在距离大于 X 时接受它,但这似乎是非常暴力的解决方案。而且leventshtein只检查替换,所以两个不同的字符仍然可以相邻。

我目前的解决方案:

--PostgreSQL 9.5    
with t as (
select 
  generate_series(1, 200) as id,
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1) ||
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1) ||
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1) ||
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1) ||
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1) ||
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1) ||
  substr('abcdefghijklmnopqrstuvwxyz0123456789', trunc(random() * 36)::integer + 1, 1)     
    as rnd_string
)


select distinct id, rnd_string from (

select t1.id, t1.rnd_string, levenshtein(t1.rnd_string, t2.rnd_string)
from t  t1
join t  t2 on t1.id < t2.id
where levenshtein(t1.rnd_string, t2.rnd_string) > 3
) x

order by id

对于 200 个 ID,它只过滤列表中的一两个字符串,但随着记录的增加,它会增长。

相关问题:

【问题讨论】:

  • 至少有两个不同的字符?什么 md5input 只会返回一个字符?
  • @EvanCarroll 如果我将生成 2000 万个长度为 7 的唯一字符串,我想确保没有任何两个这样的字符串仅在一个字符或两个相邻字符上有所不同。
  • 这将是一个很棒的prolog 问题,使用clpfd(有限域上的约束逻辑编程);糟糕的是,您将其限制为postgresql
  • 我还不知道模式是否可以使用正则语法定义,从而使用正则表达式定义。如果是这样,那么可以从正则表达式生成模式的工具应该可以工作。正如我通常使用 Prolog 做的那样,大多数人不想学习 Prolog,我找到了 regldg,但从未使用过它。一个相关且有趣的问题是语法规则,例如Chomsky 层次结构type-3 或其他 Chomsky 层次结构类型。
  • 你能用所有这些数据生成一个表,然后随意查询吗?我有几个非常接近的算法,但是我相信即使是您的 lechenstein 算法也存在缺陷(可能存在不符合您要求的记录)-假设您的要求意味着所有生成的字符串必须通过以下方式与所有其他生成的字符串区分开来2 个或更多不连续的字符。

标签: algorithm postgresql random


【解决方案1】:

base-36 中有 78,364,164,096 个七位数字。如果你以 36 进制的 101 步长(或十进制的 1297)遍历它们,你仍然有 60,419,555 个不同的数字,它们在至少 2 个不连续的地方都是不同的。

aaaaaaa
aaaabab
aaaacac
...
aaaa9a9
aaababa
aaabbbb
aaabcbc
aaabdbd
...
aaab9b9
aaacaca
aaacbcb
aaacccc
...
aaa9999
aababaa
aabacab
aabadac
aabaead
...

如果您认为该模式过于明显,请从范围中间的某处开始取 2000 万个连续数字,这样就没有人有以“aaaa”开头的数字,或者使用不同的步长,例如 107(请参阅下面的更新)。


关于 101 以外的步长36 (1297):

base-36 中的数字有以下句点:

digit:    1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18
period:  36  18  12   9  36   6  36   9   4  18  36   3  36  18  12   9  36   2

digit:   19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35
period:  36   9  12  18  36   3  36  18   4   9  36   6  36   9  12  18  36

选择形状x0y 36 的阶跃尺寸时,最小的数字y绝对不应比第三位数字x的周期少。例如步长 10636 (1302) 将是一个糟糕的选择,因为在六步之后,您只会在两个相邻的数字上得到差异:

aaaaaaa, aaaabag, aaaacam, aaaadas, aaaaeay, aaaafa4, aaaagba  

如果你想得到至少 2000 万个结果,最大步长为 3.0.2936 (3917),所以你可以得到以下选项:

1.0.1  (1297)  1.0.5  (1301)  1.0.7  (1303)  1.0.11 (1307)  1.0.13 (1309)  1.0.17 (1313)  
1.0.19 (1315)  1.0.23 (1319)  1.0.25 (1321)  1.0.29 (1325)  1.0.31 (1327)  1.0.35 (1331)  
2.0.1  (2593)  2.0.2  (2594)  2.0.5  (2597)  2.0.7  (2599)  2.0.10 (2602)  2.0.11 (2603)  
2.0.13 (2605)  2.0.14 (0606)  2.0.17 (2609)  2.0.19 (2611)  2.0.22 (2614)  2.0.23 (2615)  
2.0.25 (2617)  2.0.26 (2618)  2.0.29 (2621)  2.0.31 (2623)  2.0.34 (2626)  2.0.35 (2627)  
3.0.1  (3889)  3.0.2  (3890)  3.0.3  (3891)  3.0.5  (3893)  3.0.7  (3895)  3.0.10 (3898)  
3.0.11 (3899)  3.0.13 (3901)  3.0.14 (3902)  3.0.15 (3903)  3.0.17 (3905)  3.0.19 (3907)  
3.0.21 (3909)  3.0.22 (3910)  3.0.23 (3911)  3.0.25 (3913)  3.0.26 (3914)  3.0.29 (3917)  

另外,第二个数字可以被赋予一个不为零的值,例如1.5.736 (1483) 或 1.25.736 (2203) 以进一步混淆模式。


进一步增加感知随机性的一种简单方法是重新排列字母表。下面的例子汇集了所有的想法:它使用了一个打乱的字母表,步长为 1.13.2536 (1789),从 0.1.2.3.4.5.636子>。运行代码 sn -p 可以查看超过 4000 万个字符串中的前 10000 个。

var alphabet = "nes2jf7tkd4ha6grlz9qm0bxp8w1ovi3u5cy";
var base = 36;
var digits = 7;
var step = 1789;                                // 1.13.25 (base-36)
var number = step * Math.ceil(63970746 / step); // skip to 0.1.2.3.4.5.6 (base-36)
for (var i = 0; i < 10000; i++) {
    var n = number;
    var str = "";
    for (var j = 0; j < digits; j++) {
        var d = n % base;
        n = (n - d) / base;
        str = alphabet.charAt(d) + str;
    }
    document.write(("nnnnnn" + str).substr(-digits) + ", "); // pad with zero character
    number += step;
}

由于该方法是可逆的,并且您可以将每个字符串转换回一个应该是步长倍数的数字,您可以将其用作简单的第一次检查,以查看是否输入了 id 字符串,例如在线表格有效。

【讨论】:

    【解决方案2】:

    对我来说,这听起来很像Hamming Code。但是,您仍然必须确保生成的基本词不会发生冲突。在真正的随机设备上,您必须检查是否相等。除了检查,您还可以尝试找到确定性的伪随机序列。

    【讨论】:

      【解决方案3】:
      CREATE OR REPLACE FUNCTION number_to_base(num BIGINT, base INTEGER)
        RETURNS TEXT
        LANGUAGE sql
        IMMUTABLE
        STRICT
      AS $function$
      WITH RECURSIVE n(i, n, r) AS (
          SELECT -1, num, 0
        UNION ALL
          SELECT i + 1, n / base, (n % base)::INT
          FROM n
          WHERE n > 0
      )
      SELECT string_agg(ch, '')
      FROM (
        SELECT CASE
                 WHEN r=0 then 'z'
                 WHEN r=1 then 'q'
                 WHEN r=2 then 'w'
                 WHEN r=3 then 'k'
                 WHEN r=4 then 's'
                 WHEN r=5 then 'g'
                 WHEN r=6 then 'v'
                 WHEN r=7 then '2'
                 WHEN r=8 then '7'
                 WHEN r=9 then 'l'
                 WHEN r=10 then 'b'
                 WHEN r=11 then 'p'
                 WHEN r=12 then 'n'
                 WHEN r=13 then 'h'
                 WHEN r=14 then '1'
                 WHEN r=15 then '3'
                 WHEN r=16 then 'm'
                 WHEN r=17 then 'o'
                 WHEN r=18 then 'e'
                 WHEN r=19 then 'u'
                 WHEN r=20 then 'r'
                 WHEN r=21 then 'i'
                 WHEN r=22 then '4'
                 WHEN r=23 then 'j'
                 WHEN r=24 then 'y'
                 WHEN r=25 then '0'
                 WHEN r=26 then 'd'
                 WHEN r=27 then 'x'
                 WHEN r=28 then 'f'
                 WHEN r=29 then '9'
                 WHEN r=30 then '5'
                 WHEN r=31 then '8'
                 WHEN r=32 then '6'
                 WHEN r=33 then 't'
                 WHEN r=34 then 'c'
                 WHEN r=35 then 'a'
                 WHEN r=36 then 'C'
                 WHEN r=37 then 'E'
                 WHEN r=38 then 'Z'          
                 WHEN r=39 then 'H'
                 WHEN r=40 then 'Y'
                 WHEN r=41 then 'I'
                 WHEN r=42 then 'W'
                 WHEN r=43 then 'Q'
                 WHEN r=44 then 'M'
                 WHEN r=45 then 'L'
                 WHEN r=46 then 'P'
                 WHEN r=47 then 'O'
                 WHEN r=48 then 'K'
                 WHEN r=49 then 'X'
                 WHEN r=50 then 'S'
                 WHEN r=51 then 'A'
                 WHEN r=52 then 'U'
                 WHEN r=53 then 'R'
                 WHEN r=54 then 'V'
                 WHEN r=55 then 'G'
                 WHEN r=56 then 'B'
                 WHEN r=57 then 'D'
                 WHEN r=58 then 'N'
                 WHEN r=59 then 'J'
                 WHEN r=60 then 'F'
                 WHEN r=61 then 'T'
                 ELSE '%'
               END ch
        FROM n
        WHERE i >= 0
        ORDER BY i DESC
      ) ch
      $function$;
      
      
      select 
      number_to_base((((gs % 
      9)+1)::text||reverse(((gs*107)+20000000)::text))::bigint,62), gs
      from generate_series(1,1000) gs
      

      107 用于创建 2 个字符差异。 2000 万硬编码用于确保您拥有 20 Mil 代码,它们都具有相同的位数。其他功能用于帮助使数据看起来是伪随机的,但是该过程完全可以使用数据进行反转,或者根据 ID 重新生成。

      出于测试目的,我将结果限制为 1000。它比其他解决方案更长,但看起来更随机。可能是一种更优雅的方式,但这是快速且可重复的。

      【讨论】:

        猜你喜欢
        • 2011-09-17
        • 1970-01-01
        • 2022-11-16
        • 1970-01-01
        • 2010-11-27
        • 2012-03-10
        • 1970-01-01
        • 2018-05-08
        • 1970-01-01
        相关资源
        最近更新 更多