这是一个关于非数学家的组合学的问题,所以请尽量忍受我!
给定n个不同字符的数组,我想以最小变化顺序生成k个字符的子集,即生成i + 1恰好包含一个不在第i代中的字符的顺序。这本身并不太难。但是,我还想最大化生成i + 1中交换 out 的字符与第i代中 中交换的字符数相同的情况。为了说明,对于n = 7,k = 3:
abc abd abe * abf * abg * afg aeg * adg * acg * acd ace * acf * aef adf * ade bde bdf bef bcf * bce bcd * bcg * bdg beg * bfg * cfg ceg * cdg * cde cdf * cef def deg dfg efg
带星号的字符串表示我想要最大化的情况;例如abe中第3代的新e替换了第2代abd中的新内容。似乎不可能在每一代都发生这种情况,但我希望它尽可能频繁地发生。
我使用的典型阵列大小为20-30,子集大小大约为5-8。
我使用的是一种奇怪的语言,Icon(或实际上是它的衍生Unicon),所以我不希望任何人发布我可以直接使用的代码。但我会很感激伪代码的答案或提示,并会尽力翻译C等。另外,我注意到这类问题经常以整数数组的形式讨论,我当然可以应用解决方案以这样的方式发布我自己的问题。
由于
金巴斯汀编辑2010年6月15日:
我似乎进入了比我想象的更深的水,虽然我很感激所有答案,但并非所有答案都相关。作为一个不充分的解决方案的例子,让我发布我自己的Unicon程序,以最小的变化顺序生成字符集的k元子集。要理解代码,你需要知道的事情是:前置*表示结构的大小,所以如果s是字符串,* s表示s的大小(它包含的字符数)。 ||是一个字符串连接操作。一个前置!产生结构的每个元素,例如,字符串的每个字符,依次是连续的传递。 'suspend'控制结构从一个过程返回一个结果,但是让过程“悬而未决”,所有的局部变量就位,这样如果在一个循环中调用过程就可以产生新的结果。
procedure revdoor(s, k)
# Produces all k-subsets of a string or character set s in a 'revolving
# door' order. Each column except the first traverses the characters
# available to it in alphabetical and reverse alphabetical order
# alternately. The order of the input string is preserved.
# If called in a loop as revdoor("abcdefg", 3),
# the order of production is: abc, abd, abe, abf, abg, acg, acf, ace, acd,
# ade, adf, adg, aeg, aef, afg, bfg, bef, beg, bdg, bdf, bde, bcd, bce,
# bcf, bcg, cdg, cdf, cde, cef, ceg, cfg, dfg, deg, def, efg
local i
static Ctl
if /Ctl then { # this means 'if Ctl doesn't exist'
if k = 0 then return ""
Ctl := list(k, 1) # a list of k elements, each initialised to 1.
}
if Ctl[k] = 1 then {
if k = 1 then suspend !s else
every i := 1 to *s-k+1 do {
suspend s[i] || revdoor(s[i+1:0], k-1)
}
} else {
if k = 1 then suspend !reverse(s) else
every i := -k to -*s by -1 do {
suspend s[i] || revdoor(s[i+1:0], k-1)
}
}
# the following line multiplies element k of Ctl by -1 if k < size of Ctl
# (this controls the order of generation of characters),
# and destroys Ctl on final exit from the procedure.
if k < *Ctl then Ctl[k] *:= -1 else Ctl := &null
end
请注意,上述程序的输出在我看来并不是最佳的。到目前为止,我调查的一个结果是,n个元素的k元子集列表的最大“交换分数”不小于comb(n-1,k),或者在n = 7的情况下,k =在图3中,最大分数至少是comb(6,3)= 20.我将列表的“交换分数”定义为列表中的项目数,其中新元素替换上一项中本身是新的元素。我没有数学设备来证明这一点,但很容易看出k = 1或k = 2。对于某些(n,k),可能会略高一些,如n = 7,k = 3:
abc abd abe abf abg
acg adg aeg afg
efg dfg cfg bfg
求bdg bcg
bcd bce bcf
bdf bef
def cef aef
adf acf
acd ace
阿德
bde cde
cdf cdg
CEG
deg(交换分数= 21)
可以注意到,上面的列表是“强烈的最小变化顺序”(比如单词高尔夫:新角色总是与它所替换的角色位于同一位置),这可能表明我自己的工作正在采取的方向。我希望在几天内发布更多内容。
金
答案 0 :(得分:1)
这很简单。为了最大化替换,只需将字符视为数字,然后将字符串递增1,直到达到上限。 然后检查字符串中是否有两次使用相同的字符。 我认为这样可行:
char c[] = {'a', 'b', 'c', 'd', 'e'};
const int n = 5;
const int k = 3;
char s[k];
void print()
{
for( int i = 0; i < k; ++i )
putchar(c[s[i]]);
putchar('\n');
}
bool increment( int m )
{
// reached the limit?
if( ++s[m] == n && m == 0 )
return false;
next:
for( int i = 0; i < m; ++i )
{
if( s[m] == n )
{
// carry
s[m] = 0;
if( !increment( m-1 ))
return false;
goto next;
}
else if( s[i] == s[m] )
{
// the character is already used
++s[m];
goto next;
}
}
return true;
}
int main(int, char**)
{
// initialise
for( int i = 0; i < k; ++i )
s[i] = i;
// enumerate all combinations
do
print();
while(increment(k-1));
}
答案 1 :(得分:0)
public class Homework
{
/**
* Prints all k-combinations of a set of n elements. Answer to this
* question: http://stackoverflow.com/questions/2698551
*/
public static void main(String[] args)
{
Combinations combinations = new Combinations(7, 3);
System.out.printf(
"Printing all %d %d-combinations of a set with %d elements:\n",
combinations.size(), combinations.k, combinations.n);
for (int[] c : combinations)
System.out.println(Arrays.toString(c));
}
/**
* Provides an iterator for all k-combinations of a set of n elements.
*/
static class Combinations implements Iterable<int[]>
{
public final int n, k;
public Combinations(int n, int k)
{
if (n < 1 || n < k)
throw new IllegalArgumentException();
this.n = n;
this.k = k;
}
@Override
public Iterator<int[]> iterator()
{
return new Iterator<int[]>()
{
private int[] c;
@Override
public void remove() { throw new UnsupportedOperationException(); }
@Override
public int[] next()
{
if (c == null)
{
c = new int[k];
for (int i = 0; i < k; i++)
c[i] = i;
}
else
{
int i = c.length - 1;
while (i >= 0 && c[i] == n - k + i)
i--;
if (i < 0)
throw new NoSuchElementException();
c[i]++;
for (int j = i + 1; j < c.length; j++)
c[j] = c[i] + j - i;
}
return c.clone(); // remove defensive copy if performance is more important
}
@Override
public boolean hasNext() { return c == null || c[0] < n - k; }
};
}
/**
* Returns number of combinations: n! / (k! * (n - k)!).
*/
public BigInteger size()
{
BigInteger s = BigInteger.valueOf(n);
for (int i = n - 1; i > n - k; i--)
s = s.multiply(BigInteger.valueOf(i));
for (int i = k; i > 1; i--)
s = s.divide(BigInteger.valueOf(i));
return s;
}
}
}
以下是您的示例的输出:
Printing all 35 3-combinations of a set with 7 elements:
[0, 1, 2] [0, 1, 3] [0, 1, 4] [0, 1, 5] [0, 1, 6] [0, 2, 3] [0, 2, 4] [0, 2, 5] [0, 2, 6] [0, 3, 4]
[0, 3, 5] [0, 3, 6] [0, 4, 5] [0, 4, 6] [0, 5, 6] [1, 2, 3] [1, 2, 4] [1, 2, 5] [1, 2, 6] [1, 3, 4]
[1, 3, 5] [1, 3, 6] [1, 4, 5] [1, 4, 6] [1, 5, 6] [2, 3, 4] [2, 3, 5] [2, 3, 6] [2, 4, 5] [2, 4, 6]
[2, 5, 6] [3, 4, 5] [3, 4, 6] [3, 5, 6] [4, 5, 6]
答案 2 :(得分:0)
我没有从算法开始,而是试图找到一种方法来找到最大“交换分数”的表单,这样你就知道要拍什么了。通常,用于产生期望结构的算法可以从这样的证明中出现。
从大学开始已经有很长一段时间了,但是我试着想出一个有助于解决这个问题的组合模型,而且没有太多运气。
我开始将组合集想象为图中的顶点,边缘对应于组合的“邻接”(仅一个元素差异)。所以:
这些图表有很多对称性。对于任何给定的{n,k},该图与{n,n-k}的图相同。如果k = 1或k = n-1,则它是n个顶点上的完整图形(每个组合与所有其他组合仅相差一个字符)。但是,我无法从中看到明显的算法。
编辑:我的下一个想法是设想图表略有不同的解释。您可以将每个{n,k}组合视为具有k 1的n位序列。 1s的位置对应于组合中存在n个字符中的哪一个。因此,对于n = 7 k = 3,abc是1110000,adg是1001001,efg是0000111.这样你也可以想象位于n维超立方体角落的点。因此,对于给定的子序列,如果它们是共面的,则边缘匹配您的“最小交换”标准:我将它们视为通过超立方体的“切割平面”。
您正在寻找通过此组合图表的汉密尔顿路径,符合您的特殊标准。
另一种思考问题的方法是尽量减少序列中做更改组合中哪个字符被更改的次数。
答案 3 :(得分:0)
为了得到一个好的答案,可以同时计算组合列表是否可以接受,或者您是否需要一次计算一个?换句话说,你需要一个功能:
Combination nextCombo();
或
vector<Combination> allCombinations();
可以接受吗?
如果批量计算组合是允许的,那么迭代加深的A *搜索(或者只是A *搜索,但我怀疑它的内存耗尽)可能会起作用。通过允许的启发式,A *可以保证最佳。我的时间不够,所以如果我有时间编写代码,我决定发布这个部分答案并编辑帖子。
A *是图搜索算法。在这种情况下,节点是到目前为止使用的组合列表(列表中不允许重复)。我的计划是为节点使用位串表示。 n = 30将适合32位整数。我们可以随意置换任何解决方案,以便第一个组合以0开头并以1结尾,即000 ... 1111。具有较短列表的节点连接到较长的一个节点,如果两个列表相同直到最后一个元素,并且最后一个元素仅在一个0'位翻转为1并且一个1位翻转为0时不同。如果存在“交换”,则两者之间的距离为0;如果存在交换,则距离为1。
每个组合的第二个表示形式是打开的位的排序列表。此图的一种可能的允许启发式是使用此索引列表。每次(在组合列表中)索引在索引列表中的特定位置使用,将其标记为关闭。具有未使用指数的头寸数量 - 当前最后更改的指数是(我相信)需要发生的最小交换数量。
为了说明这种启发式方法,请考虑上面的序列abc abd abe * abf * abg afg。 (字母将是我治疗中的数字,但这是一个小的差异)。此序列(将是搜索图中的一个节点)将标记以下位置:
1 2 3 *a b *b c c *c d d *d e e *e *f *f *g
因此,启发式预测至少需要一个交换(因为位置3中没有未标记的元素,当前位置为2)。
如果我有时间,我会编辑它以发布算法的代码和性能。
Re:NP完整性结果(在对Zac Thompson答案的评论中)。我们正在寻找最小成本哈密尔顿路径的图表具有非常特殊的结构。例如,通常NP完全哈密顿路径问题可以在O(n)时间内用“枚举所有组合”算法求解 - 其中n是图中节点的数量。这种结构使得虽然在一般图形上顶点覆盖很难,但是在图形上它可能是多项式的(甚至是线性的或二次的)。当然,由于图表有很多节点,例如n = 30,k = 8,你可能还有很多计算。
答案 4 :(得分:0)
我在2010年解决了这个问题,但当时未能找到解决方案。几天前,我又看了一下那段时间的笔记,怀疑我已经非常接近解决方案了。几分钟后,我拿到了钥匙。
重新注册:要求是字符串s的k子集的严格的最小变化排序,使得LIFO(后进先出)最大化。我在之前的帖子中称之为最大化的“交换”。
我在关键要求之后调用算法maxlifo(最大化LIFO)。它需要两个参数,一个字符串s,它不能包含重复的字符,一个正整数k不大于s的大小。算法是递归的,即maxlifo(s,k)使用maxlifo(s,k-1)的输出向下到k = 1。输出以列表形式返回。
下面我用一些例子给出一个非正式的解释,使用字符串“abcdefg”和各种k值。接下来是作为Unicon过程实现的示例。 (我不会流利使用任何比较常用的语言。)
案例k = 1是微不足道的 - 它从头到尾依次返回s的元素。
对于k&gt; 1,将以下规则应用于maxlifo(s,k-1)的输出:
(1)对于maxlifo(s,k-1)输出的每个元素,在一行中列出从该元素构建的k子集,其中每个缺失的s依次存在。子集中字符的顺序与s中相同。
(2)从第二行开始,将空白“占位符”替换为出现在前一行中的所有子集。 s的每个k子集现在只出现一次。
(3)在每个非空白行中,标记一个首字母!每个子集,使得占位符位于下一行的相同位置。这个标记意味着'第一'。每个非空白行中都会标记一个子集。
(4)删除所有完全空白的行(仅包含占位符)。
(5)在除了最后一行之外的每一行中,标记为决赛!对应于下一下一行中标记为“第一”的子集的位置中的子集。此标记表示“最后”。
现在我们可以列出子集的最终maxlifo排序。从上到下的每一行都是有序的,其元素按顺序添加到输出列表中。
(6)从上到下的每一行:
(6.1)删除所有空白占位符。
(6.2)将标记为“first”(initial!)的子集添加到输出列表中,并将其从行中删除。
(6.3)如果行中仍有子集,则最左边或最右边的子集将被标记为“last”(最终!)。如果最右边的子集标记为“last”,则按从左到右的顺序将子集添加到输出列表,否则按从右到左的顺序添加。
(7)处理完所有行后,返回输出列表。
使用maxlifo(“abcdefg”,2)的例子:
Col1包含maxlifo的输出(“abcdefg”,1)。 Col2的行包含与s:
的剩余字符形成的派系Col1 Col2
---- ----------------------------
a ab ac ad ae af ag
b ab bc bd be bf bg
c ac bc cd ce cf cg
d ad bd cd de df dg
e ae be ce de ef eg
f af bf cf df ef fg
g ag bg cg dg eg fg
删除显示在前一行中的子集:
a ab ac ad ae af ag
b bc bd be bf bg
c cd ce cf cg
d de df dg
e ef eg
f fg
g
标记每行中的“第一个”子集(下面有一个空白的子集):
a !ab ac ad ae af ag
b !bc bd be bf bg
c !cd ce cf cg
d !de df dg
e !ef eg
f !fg
g
删除所有完全空白的行(在这种情况下只有一行):
a !ab ac ad ae af ag
b !bc bd be bf bg
c !cd ce cf cg
d !de df dg
e !ef eg
f !fg
标记每行中的“最后”子集(在其下方具有“第一个”子集的那个子集)。
a !ab ac! ad ae af ag
b !bc bd! be bf bg
c !cd ce! cf cg
d !de df! dg
e !ef eg!
f !fg
按照上述顺序输出每一行:'first',unmarked,'last':
Ordered rows:
a !ab ac! ad ae af ag ab ag af ae ad ac
b !bc bd! be bf bg bc bg bf be bd
c !cd ce! cf cg cd cg cf ce
d !de df! dg de dg df
e !ef eg! ef eg
f !fg fg
输出:[ab ag af ad ac bc bg bf bf cd cg cf ce df dg de ef eg fg]
3&lt; = k&lt; = 6的实例更详细地给出。在步骤4中删除的空行将保留在原位。
maxlifo(“abcdefg”,3):
Ordered rows:
ab !abc abd abe abf abg! abc abd abe abf abg
ag acg adg aeg! !afg afg acg adg aeg
af acf adf! !aef aef acf adf
ae ace! !ade ade ace
ad !acd! acd
ac
bc !bcd bce bcf bcg! bcd bce bcf bcg
bg bdg beg! !bfg bfg bdg beg
bf bdf! !bef bef bdf
be !bde! bde
bd
cd !cde cdf cdg! cde cdf cdg
cg ceg! !cfg cfg ceg
cf !cef! cef
ce
de !def deg! def deg
dg !dfg! dfg
df
ef !efg efg
eg
fg
输出:[abc abd abe abf abg afg acg adg aeg aef acf adf ade ace acd
bcd bce bcf bcg bfg bdg beg bef bdf bde
cde cdf cdg cfg ceg cef
def deg dfg
EFG]
maxlifo(“abcdefg”,4):
Ordered rows:
abc !abcd abce! abcf abcg abcd abcg abcf abce
abd !abde abdf! abdg abde abdg abdf
abe !abef abeg! abef abeg
abf !abfg! abfg
abg
afg acfg! adfg !aefg aefg adfg acfg
acg !acdg aceg! acdg aceg
adg !adeg! adeg
aeg
aef acef! !adef adef acef
acf !acdf! acdf
adf
ade !acde! acde
ace
acd
bcd !bcde bcdf! bcdg bcde bcdg bcdf
bce !bcef bceg! bcef bceg
bcf !bcfg! bcfg
bcg
bfg bdfg! !befg befg bdfg
bdg !bdeg! bdeg
beg
bef !bdef! bdef
bdf
bde
cde !cdef cdeg! cdef cdeg
cdf !cdfg! cdfg
cdg
cfg !cefg! cefg
ceg
cef
def !defg defg
deg
dfg
efg
输出:[abcd abcg abcf abce abde abdg abdf abef abeg abfg aefg adfg acfg acdg aceg adeg adef acef acdf acde bcde bcdg bcdf bcef bceg bcfg befg bdfg bdeg bdef cdef cdeg cdfg cefg DEFG]
maxlifo(“abcdefg”,5):
Ordered rows:
abcd !abcde abcdf abcdg! abcde abcdf abcdg
abcg abceg! !abcfg abcfg abceg
abcf !abcef! abcef
abce
abde !abdef abdeg! abdef abdeg
abdg !abdfg! abdfg
abdf
abef !abefg! abefg
abeg
abfg
aefg acefg! !adefg adefg acefg
adfg !acdfg! acdfg
acfg
acdg !acdeg! acdeg
aceg
adeg
adef !acdef! acdef
acef
acdf
acde
bcde !bcdef bcdeg! bcdef bcdeg
bcdg !bcdfg! bcdfg
bcdf
bcef !bcefg! bcefg
bceg
bcfg
befg !bdefg! bdefg
bdfg
bdeg
bdef
cdef !cdefg cdefg
cdeg
cdfg
cefg
defg
输出:[abcde abcdf abcdg abcfg abceg abcef abdef abdeg abdfg abefg adefg acefg acdfg acdeg acdef bcdef bcdeg bcdfg bcefg bdefg CDEFG]
maxlifo(“abcdefg”,6):
Ordered rows:
abcde !abcdef abcdeg! abcdef abcdeg
abcdf !abcdfg! abcdfg
abcdg
abcfg !abcefg! abcefg
abceg
abcef
abdef !abdefg! abdefg
abdeg
abdfg
abefg
adefg
acefg !acdefg! acdefg
acdfg
acdeg
acdef
bcdef !bcdefg bcdefg
bcdeg
bcdfg
bcefg
bdefg
cdefg
输出:[abcdef abcdeg abcdfg abcefg abdefg acdefg bcdefg]
Unicon实施:
procedure maxlifo(s:string, k:integer)
# A solution to my combinatorics problem from 2010.
# Return a list of the k subsets of the characters of a string s
# in a minimal change order such that last-in first-out is maximised.
# String s must not contain duplicate characters and in the present
# implementation must not contain "!", which is used as a marker.
local ch, cand, Hit, inps, i, j, K, L, Outp, R, S
# Errors
if *cset(s) ~= *s then
stop("Duplicate characters in set in maxlifo(", s, ", ", k, ")")
if find("!", s) then
stop("Illegal character in set in maxlifo(", s, ", ", k, ")")
if k > *s then
stop("Subset size larger than set size in maxlifo(", s, ", ", k, ")")
# Special cases
if k = 0 then return []
if k = *s then return [s]
Outp := []
if k = 1 then {
every put(Outp, !s)
return Outp
}
# Default case
S := set()
K := []
# Build cliques from output of maxlifo(s, k-1) with the remaining
# characters in s, substituting empty strings as placeholders for
# subsets already listed.
every inps := !maxlifo(s, k-1) do {
R := []
every ch := !s do
if not find(ch, inps) then {
cand := reorder(inps ++ ch, s)
if member(S, cand) then cand := "" else insert(S, cand)
put(R, cand)
}
put(K, R)
}
# Mark ‘first’ subset in each row with initial "!"
every i := 1 to *K - 1 do {
every j := 1 to *K[i] do
if K[i, j] ~== "" & K[i+1, j] == "" then {
K[i, j] := "!" || K[i, j]
break
}
}
# Remove rows containing only placeholders
every i := *K to 1 by -1 do {
every if !K[i] ~== "" then break next
delete(K, i)
}
# Mark ‘last’ subset in each row with final "!"
every i := 1 to *K - 1 do
every j := 1 to *K[i] do
if K[i+1, j][1] == "!" then {
K[i, j] ||:= "!"
break
}
# Build output list
every R := !K do {
# Delete placeholders from row (no longer needed and in the way)
every j := *R to 1 by -1 do if R[j] == "" then delete(R, j)
# Handle ‘first’ subset and remove from row
# N.B. ‘First’ subset will be leftmost or rightmost in row
if R[1][1] == "!" then
put(Outp, trim(get(R), '!', 0))
else put(Outp, trim(pull(R), '!', 0))
# Handle any remaining subsets, ‘last’ subset last, stripping '!' markers
# N.B. ‘Last’ subset will be leftmost or rightmost in row after removal
# of ‘first’ subset.
if R[-1][-1] == "!" then while put(Outp, trim(get(R), '!', 0)) else
while put(Outp, trim(pull(R), '!', 0))
}
return Outp
end
procedure reorder(cs:cset, s:string)
# Reorder cset cs according to string s
local r
# If no s, return set in alphabetical order
if /s then return string(cs)
r := ""
s ? while tab(upto(cs)) do r ||:= move(1)
return r
end