Linq 查询获取所有数字(正数和负数)最多 N 个,总和为数字 K
Linq query to get all numbers (positive and negative) up to N that sum up to number K
我有一个特定的任务是创建一个只使用 LINQ 的函数 ,这就是它对我来说具有挑战性的原因。
问题是这样的:
生成N个数的所有正负(+-)表达式,总和为K
例如,如果我有 N = 3,K = 0。我应该在某个时候有 8 种数字组合,包括加号和减号(2 的 n 次方组合),以及一些我应该只提取的 Where 子句那些总和为 0(即 K)的人。
所以结果应该是这样的:
-1 -2 +3 = 0;
+1 +2 -3 = 0;
同样,我只能使用 LINQ 查询,不能使用其他任何东西,而且我无法解决这个问题。有人可以帮忙吗?
这是一个仅使用蛮力的骇人听闻的解决方案。我相信可以找到一个最优雅的。对于超过 30 个数字,它也将无法生成结果。
Enumerable
.Range(0, (int)Math.Pow(2, N))
.Select(i => Convert.ToString(~i, 2)
.Reverse()
.Take(N)
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
.Where(l => l.Sum() == K)
.ToList();
一些解释:
正如您所指出的,对于 N 个数字,有 2^N 种和+或-的组合。所以第一步是构建这些 2^N 组合。
Enumerable
.Range(0, (int)Math.Pow(2, N))
构造前2^N个数。让我们在这个例子中考虑 N = 10.
.Select(i => Convert.ToString(~i, 2)
将每个数字转换为其二进制字符串表示形式,但取反(0 替换为 1,1 替换为 0)。反转的目的是获取数字的所有 32 位数字,否则数字 2,即二进制中的 10 将由字符串“10”表示。我们想要一个与 N 一样长的字符串,即“0000000010”。通过这个技巧,我们得到的不是“01”,而是“1111111111111111111111111111101”。
然后该字符串被视为字符序列。
.Reverse()
该序列是相反的:“1011111111111111111111111111111”,
.Take(N)
我们只取前 N 个字符:“1011111111”。
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
然后我们将每个字符投影到一个数字,即索引+1,符号取决于字符本身:如果等于'1'加,否则-。这给出了: 1, -2, 3, 4, 5, 6, 7, 8, 9, 10 在到目前为止使用的示例中。
.Where(l => l.Sum() == K)
.ToList();
从那里我们构建了所有可能的序列,因此我们可以对这些序列求和并过滤出总和等于我们的目标的序列。
当然你可以添加一个检查:if |K| > (N^2 + N)/2,不用计算,不会没有匹配...
作为您在 Linqpad 中通过 copy/paste 尝试的查询:
void Main()
{
Utils.Calculate(10, 1)
.Select(e => new
{
Sum = e.Sum(),
Elements = e
})
.Dump();
}
// You can define other methods, fields, classes and namespaces here
public static class Utils
{
public static List<List<int>> Calculate(int N, int K)
{
// Enumerable.Range will throw ArgumentOutOfRangeException
// if N <= 0 || N > 30
return Enumerable
.Range(0, (int)Math.Pow(2, N))
.Select(i => Convert.ToString(~i, 2)
.Reverse()
.Take(N)
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
.Where(l => l.Sum() == K)
.ToList();
}
}
编辑
同样的逻辑也可以写成:
Enumerable.Range(1, (int)Math.Pow(2, N))
.Select(i => int.MaxValue - i + 1)
.Select(i => Convert.ToString(i, 2)
.Skip(31-N)
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
.Where(l => l.Sum() == K)
.ToList()
列表的顺序将与第一种方法不同,这与结果无关。
这里有一个相当简单的方法:
int n = 3;
int k = 0;
IEnumerable<int[]> query =
from i in Enumerable.Range(0, 1 << n)
let b = Convert.ToString(i, 2).PadLeft(n, '0')
let r = Enumerable.Range(1, n)
let d = r.Zip(b, (x, y) => y == '0' ? x : -x).ToArray()
let s = d.Sum()
where s == k
select d;
运行 给了我:
1, 2, -3
-1, -2, 3
并且,例如,使用 n = 9
和 k = 33
我得到:
1, 2, 3, 4, 5, -6, 7, 8, 9
1, -2, 3, -4, 5, 6, 7, 8, 9
-1, 2, 3, 4, -5, 6, 7, 8, 9
-1, -2, -3, 4, 5, 6, 7, 8, 9
这样可以避免创建字符串:
IEnumerable<bool> GetBits(int q, int v) =>
q == 0
? Enumerable.Empty<bool>()
: GetBits(q - 1, v >> 1).StartWith((v & 1) == 1);
int n = 13;
int k = 75;
IEnumerable<IEnumerable<int>> query =
from i in Enumerable.Range(0, 1 << n)
let bs = GetBits(n, i)
let rs = Enumerable.Range(1, n)
let ds = rs.Zip(bs, (x, y) => y ? x : -x)
let s = ds.Sum()
where s == k
select ds;
所以问题是关于决定从 1 到 N 的每个整数是否应该相加或相减?
public static IEnumerable<IEnumerable<int>> AllCombinations(int n, int k)
=> Enumerable.Range(0, 1 << n)
.Select(i =>
Enumerable.Range(0, n)
.Select(j => (i & 1 << j) == 0 ? j + 1 : -j - 1)
)
.Where(r => r.Sum() == k);
感谢任何试图提供帮助的人,不幸的是,所有解决方案对于这个问题都太复杂了,所以我设法找到了自己的解决方案,它更简单一些,但你的解决方案引导我朝着正确的方向前进。
IEnumerable<string> seed = new[] { "" };
var x = Enumerable.Range(0, n).Aggregate(seed, (a, _) => a.
SelectMany(s => new[] { s + "+", s + "-" }));
return x.Select((a) => a.Select((a, b) => a == '+' ? b + 1 : (b + 1) * -1))
.Where(r => r.Sum() == k);
根据 OP 的回答,这是一个替代方案,它使用 int[]
建立候选人名单。
int n = 26;
int k = 8;
var sw = Stopwatch.StartNew();
IEnumerable<int[]> seed = new[] { new int[] { } };
var xss =
Enumerable
.Range(1, n)
.Aggregate(
seed,
(a, i) => a.SelectMany(s => new[]
{
s.Append(i).ToArray(),
s.Append(-i).ToArray(),
}))
.Where(xs => xs.Sum() == k)
.ToArray();
sw.Stop();
sw.ElapsedMilliseconds.Dump();
这比 OP 的字符串构建方法更快。
我有一个特定的任务是创建一个只使用 LINQ 的函数 ,这就是它对我来说具有挑战性的原因。 问题是这样的:
生成N个数的所有正负(+-)表达式,总和为K
例如,如果我有 N = 3,K = 0。我应该在某个时候有 8 种数字组合,包括加号和减号(2 的 n 次方组合),以及一些我应该只提取的 Where 子句那些总和为 0(即 K)的人。 所以结果应该是这样的:
-1 -2 +3 = 0;
+1 +2 -3 = 0;
同样,我只能使用 LINQ 查询,不能使用其他任何东西,而且我无法解决这个问题。有人可以帮忙吗?
这是一个仅使用蛮力的骇人听闻的解决方案。我相信可以找到一个最优雅的。对于超过 30 个数字,它也将无法生成结果。
Enumerable
.Range(0, (int)Math.Pow(2, N))
.Select(i => Convert.ToString(~i, 2)
.Reverse()
.Take(N)
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
.Where(l => l.Sum() == K)
.ToList();
一些解释:
正如您所指出的,对于 N 个数字,有 2^N 种和+或-的组合。所以第一步是构建这些 2^N 组合。
Enumerable
.Range(0, (int)Math.Pow(2, N))
构造前2^N个数。让我们在这个例子中考虑 N = 10.
.Select(i => Convert.ToString(~i, 2)
将每个数字转换为其二进制字符串表示形式,但取反(0 替换为 1,1 替换为 0)。反转的目的是获取数字的所有 32 位数字,否则数字 2,即二进制中的 10 将由字符串“10”表示。我们想要一个与 N 一样长的字符串,即“0000000010”。通过这个技巧,我们得到的不是“01”,而是“1111111111111111111111111111101”。
然后该字符串被视为字符序列。
.Reverse()
该序列是相反的:“1011111111111111111111111111111”,
.Take(N)
我们只取前 N 个字符:“1011111111”。
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
然后我们将每个字符投影到一个数字,即索引+1,符号取决于字符本身:如果等于'1'加,否则-。这给出了: 1, -2, 3, 4, 5, 6, 7, 8, 9, 10 在到目前为止使用的示例中。
.Where(l => l.Sum() == K)
.ToList();
从那里我们构建了所有可能的序列,因此我们可以对这些序列求和并过滤出总和等于我们的目标的序列。
当然你可以添加一个检查:if |K| > (N^2 + N)/2,不用计算,不会没有匹配...
作为您在 Linqpad 中通过 copy/paste 尝试的查询:
void Main()
{
Utils.Calculate(10, 1)
.Select(e => new
{
Sum = e.Sum(),
Elements = e
})
.Dump();
}
// You can define other methods, fields, classes and namespaces here
public static class Utils
{
public static List<List<int>> Calculate(int N, int K)
{
// Enumerable.Range will throw ArgumentOutOfRangeException
// if N <= 0 || N > 30
return Enumerable
.Range(0, (int)Math.Pow(2, N))
.Select(i => Convert.ToString(~i, 2)
.Reverse()
.Take(N)
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
.Where(l => l.Sum() == K)
.ToList();
}
}
编辑 同样的逻辑也可以写成:
Enumerable.Range(1, (int)Math.Pow(2, N))
.Select(i => int.MaxValue - i + 1)
.Select(i => Convert.ToString(i, 2)
.Skip(31-N)
.Select((c, i) => c == '1' ? i + 1 : -(i + 1))
.ToList())
.Where(l => l.Sum() == K)
.ToList()
列表的顺序将与第一种方法不同,这与结果无关。
这里有一个相当简单的方法:
int n = 3;
int k = 0;
IEnumerable<int[]> query =
from i in Enumerable.Range(0, 1 << n)
let b = Convert.ToString(i, 2).PadLeft(n, '0')
let r = Enumerable.Range(1, n)
let d = r.Zip(b, (x, y) => y == '0' ? x : -x).ToArray()
let s = d.Sum()
where s == k
select d;
运行 给了我:
1, 2, -3
-1, -2, 3
并且,例如,使用 n = 9
和 k = 33
我得到:
1, 2, 3, 4, 5, -6, 7, 8, 9
1, -2, 3, -4, 5, 6, 7, 8, 9
-1, 2, 3, 4, -5, 6, 7, 8, 9
-1, -2, -3, 4, 5, 6, 7, 8, 9
这样可以避免创建字符串:
IEnumerable<bool> GetBits(int q, int v) =>
q == 0
? Enumerable.Empty<bool>()
: GetBits(q - 1, v >> 1).StartWith((v & 1) == 1);
int n = 13;
int k = 75;
IEnumerable<IEnumerable<int>> query =
from i in Enumerable.Range(0, 1 << n)
let bs = GetBits(n, i)
let rs = Enumerable.Range(1, n)
let ds = rs.Zip(bs, (x, y) => y ? x : -x)
let s = ds.Sum()
where s == k
select ds;
所以问题是关于决定从 1 到 N 的每个整数是否应该相加或相减?
public static IEnumerable<IEnumerable<int>> AllCombinations(int n, int k)
=> Enumerable.Range(0, 1 << n)
.Select(i =>
Enumerable.Range(0, n)
.Select(j => (i & 1 << j) == 0 ? j + 1 : -j - 1)
)
.Where(r => r.Sum() == k);
感谢任何试图提供帮助的人,不幸的是,所有解决方案对于这个问题都太复杂了,所以我设法找到了自己的解决方案,它更简单一些,但你的解决方案引导我朝着正确的方向前进。
IEnumerable<string> seed = new[] { "" };
var x = Enumerable.Range(0, n).Aggregate(seed, (a, _) => a.
SelectMany(s => new[] { s + "+", s + "-" }));
return x.Select((a) => a.Select((a, b) => a == '+' ? b + 1 : (b + 1) * -1))
.Where(r => r.Sum() == k);
根据 OP 的回答,这是一个替代方案,它使用 int[]
建立候选人名单。
int n = 26;
int k = 8;
var sw = Stopwatch.StartNew();
IEnumerable<int[]> seed = new[] { new int[] { } };
var xss =
Enumerable
.Range(1, n)
.Aggregate(
seed,
(a, i) => a.SelectMany(s => new[]
{
s.Append(i).ToArray(),
s.Append(-i).ToArray(),
}))
.Where(xs => xs.Sum() == k)
.ToArray();
sw.Stop();
sw.ElapsedMilliseconds.Dump();
这比 OP 的字符串构建方法更快。