如何(从固定列表中)选择一个数字序列,该序列将与目标数字相加

How to pick a sequence of numbers (from a fixed list) that will sum to a target number?

本文关键字:数字 目标 字序 一个 列表 选择 如何      更新时间:2023-10-16

假设我有一个目标数字和一个可能的值列表,我可以选择这些值来创建一个序列,一旦对每个选择的数字求和,就会对目标进行求和:

target = 31
list = 2, 3, 4
possible sequence: 3 2 4 2 2 2 4 2 3 2 3 2 

我想:

  1. 首先决定是否有任何序列可以到达目标
  2. 返回多个(可能的(序列中的一个

这是我的尝试:

#include <iostream>
#include <random>
#include <chrono>
#include <vector>
inline int GetRandomInt(int min = 0, int max = 1) {
uint64_t timeSeed = std::chrono::high_resolution_clock::now().time_since_epoch().count();
std::seed_seq ss{ uint32_t(timeSeed & 0xffffffff), uint32_t(timeSeed >> 32) };
std::mt19937_64 rng;
rng.seed(ss);
std::uniform_int_distribution<int> unif(min, max);
return unif(rng);
}
void CreateSequence(int target, std::vector<int> &availableNumbers) {
int numAttempts = 1;
int count = 0;
std::vector<int> elements;
while (count != target) {
while (count < target) {
int elem = availableNumbers[GetRandomInt(0, availableNumbers.size() - 1)];
count += elem;
elements.push_back(elem);
}
if (count != target) {
numAttempts++;
count = 0;
elements.clear();
}
}
int size = elements.size();
std::cout << "count: " << count << " | " << "num elements: " << size << " | " << "num attempts: " << numAttempts << std::endl;
for (auto it = elements.begin(); it != elements.end(); it++) {
std::cout << *it  << " ";
}   
}
int main() {
std::vector<int> availableNumbers = { 2, 3, 4 };
CreateSequence(31, availableNumbers);
}

但如果数字列表不适合达到这样的总和,它可以无限循环;示例:

std::vector<int> availableNumbers = { 3 };
CreateSequence(8, availableNumbers);

没有一个3的序列和为8。此外,如果列表很大,而目标数量很高,则可能会导致大量的处理(导致大量检查失败(。

你将如何实现这种算法?

您建议的代码可能非常快,因为它是启发式的。但正如你所说,它可能会陷入一个几乎无穷无尽的循环中。

如果你想避免这种情况,你必须搜索一整套可能的组合。

抽象

让我们将我们的算法定义为函数f,标量目标t和向量<b>作为返回系数向量<c>的参数,其中<b><c>具有相同的维度:
<c> = f(t, <b>)

首先,给定的数集Sg应该被约简为它们的约简集Sr,因此我们降低了解向量<c>的维数。例如,{2,3,4,11}可以被缩减为{2,3}。我们通过递归调用我们的算法来获得这一点,方法是将Sg拆分为一个新的目标ti,剩余的数字作为新的给定集合Sgi,并询问算法是否找到任何解(非零向量(。如果是,则从原始给定集合Sg中移除该目标ti。递归地重复此操作,直到找不到任何解决方案为止。

现在我们可以将这组数字理解为多项式,其中我们正在寻找可能的系数ci,以获得我们的目标t。让我们用i={1..n}调用Sb中的每个元素bi

我们的测试和tsci * bi的所有i的和,其中每个ci可以从0运行到ni = floor(t/bi)

可能的测试次数N现在是所有ni+1:N = (n1+1) * (n2+1) * ... * (ni+1)的乘积。

现在,通过将系数向量<c>表示为整数向量并递增c1,并将超限转移到向量中的下一个元素,重置c1等,对所有可能性进行迭代。

示例

#include <random>
#include <chrono>
#include <vector>
#include <iostream>
using namespace std;
static int evaluatePolynomial(const vector<int> &base, const vector<int> &coefficients)
{
int v=0;
for(unsigned long i=0; i<base.size(); i++){
v += base[i]*coefficients[i];
}
return v;
}
static bool isZeroVector(vector<int> &v)
{
for (auto it = v.begin(); it != v.end(); it++) {
if(*it != 0){
return false;
}
} 
return true;
}
static vector<int> searchCoeffs(int target, vector<int> &set) {
// TODO: reduce given set
vector<int> n = set;
vector<int> c = vector<int>(set.size(), 0);
for(unsigned long int i=0; i<set.size(); i++){
n[i] = target/set[i];
}
c[0] = 1;
bool overflow = false;
while(!overflow){
if(evaluatePolynomial(set, c) == target){
return c;
}
// increment coefficient vector
overflow = true;
for(unsigned long int i=0; i<c.size(); i++){
c[i]++;
if(c[i] > n[i]){
c[i] = 0;
}else{
overflow = false;
break;
}
}
}
return vector<int>(set.size(), 0);
}
static void print(int target, vector<int> &set, vector<int> &c)
{
for(unsigned long i=0; i<set.size(); i++){
for(int j=0; j<c[i]; j++){
cout << set[i] << " ";
}
}
cout << endl;
cout << target << " = ";
for(unsigned long i=0; i<set.size(); i++){
cout << " +" << set[i] << "*" << c[i];
}
cout << endl;
}
int main() {
vector<int> set = {4,3,2};
int target = 31;
auto c = searchCoeffs(target, set);
print(target, set,c);
}

该代码打印

4 4 4 4 4 4 4 3 
31 =  +4*7 +3*1 +2*0

进一步思考

  • 生产代码应该测试任何给定值中的零
  • 如果评估的多项式已经超过目标值,则可以通过增加下一个系数来改进搜索
  • 当c1被设置为零时,当计算目标值和评估多项式的差并且检查该差是否是b1的倍数时,可以进一步加速。如果不是,c2可以直接递增
  • 也许存在一些利用最小公倍数的捷径

正如ihavenovidea所建议的,我也会尝试回溯。此外,我将按递减顺序对数字进行排序,以加快处理速度。

注意:评论比回答更合适,但我不允许这样做。希望它能有所帮助。如果有人要求,我将不回答这个问题。