ค้นหาตัวเลขที่เป็นไปได้ในอาร์เรย์ที่สามารถรวมเข้ากับค่าเป้าหมายได้

เนื่องจากฉันมีอาร์เรย์ของตัวเลขเช่น [14,6,10] - ฉันจะค้นหาชุดค่าผสม/คู่ที่เป็นไปได้ที่สามารถเพิ่มเกินค่าเป้าหมายที่กำหนดได้อย่างไร

ตัวอย่างเช่นฉันมี [14,6,10] ฉันกำลังค้นหาค่าเป้าหมายที่ 40 ผลลัพธ์ที่คาดหวังของฉันจะเป็น

 10 + 10 + 6 + 14
 14 + 14 + 6 + 6
 10 + 10 + 10 + 10

*การสั่งซื้อไม่สำคัญ

จากที่กล่าวมานี่คือสิ่งที่ฉันได้ลองมาแล้ว:

function Sum(numbers, target, partial) {
  var s, n, remaining;

  partial = partial || [];

  s = partial.reduce(function (a, b) {
    return a + b;
  }, 0);

  if (s === target) {
     console.log("%s", partial.join("+"))
  }


  for (var i = 0; i < numbers.length; i++) {
    n = numbers[i];
    remaining = numbers.slice(i + 1);
    Sum(remaining, target, partial.concat([n]));
  }
}

>>> Sum([14,6,10],40);
// returns nothing

>>> Sum([14,6,10],24);
// return 14+10

จริงๆ แล้วมันไม่มีประโยชน์เพราะมันจะกลับมาก็ต่อเมื่อสามารถใช้ตัวเลขได้เพียงครั้งเดียวเพื่อสรุปผล

แล้วจะทำยังไงล่ะ?


person user7716943    schedule 09.02.2019    source แหล่งที่มา
comment
คุณมีเลขลบด้วยเหรอ?   -  person Nina Scholz    schedule 09.02.2019
comment
@Nina มีเพียงจำนวนบวกเท่านั้นไม่มีค่าลบ   -  person user7716943    schedule 09.02.2019
comment
[14,6,10] ความยาว+1 นั่นคือตัวเลขรวมกัน 4 ตัวใช่ไหม นอกจากนี้ จะเกิดอะไรขึ้นถ้าคุณมีรายการซ้ำในอาร์เรย์ของคุณ   -  person Monica Acha    schedule 09.02.2019


คำตอบ (2)


คุณสามารถเพิ่มค่าของดัชนีจริงได้ตราบใดที่ผลรวมน้อยกว่าผลรวมที่ต้องการหรือดำเนินการกับดัชนีถัดไป

function getSum(array, sum) {
    function iter(index, temp) {
        var s = temp.reduce((a, b) => a + b, 0);
        if (s === sum) result.push(temp);
        if (s >= sum || index >= array.length) return;
        iter(index, temp.concat(array[index]));
        iter(index + 1, temp);
    }

    var result = [];
    iter(0, []);
    return result;
}

console.log(getSum([14, 6, 10], 40));
.as-console-wrapper { max-height: 100% !important; top: 0; }

หากต้องการรับชุดผลลัพธ์ที่จำกัด คุณสามารถระบุความยาวและตรวจสอบในเงื่อนไขการออก

function getSum(array, sum, limit) {
    function iter(index, temp) {
        var s = temp.reduce((a, b) => a + b, 0);
        if (s === sum) result.push(temp);
        if (s >= sum || index >= array.length || temp.length >= limit) return;
        iter(index, temp.concat(array[index]));
        iter(index + 1, temp);
    }

    var result = [];
    iter(0, []);
    return result;
}

console.log(getSum([14, 6, 10], 40, 5));
.as-console-wrapper { max-height: 100% !important; top: 0; }

person Nina Scholz    schedule 09.02.2019
comment
ตรงกับที่ฉันต้องการ! - person user7716943; 09.02.2019
comment
สิ่งหนึ่งที่ถ้าฉันชอบ getSum([1, 2, 3], 20) ฉันจะส่งออกผลลัพธ์อย่างชัดเจนได้อย่างไรว่าถ้าความยาวของอาร์เรย์เท่ากับ / ต่ำกว่า x - person user7716943; 09.02.2019
comment
เพิ่มเงื่อนไขที่นี่: if (s >= sum || index >= array.length || temp.length >= x) return; นี่คือเงื่อนไขทางออกสำหรับการคืนชีพ - person Nina Scholz; 09.02.2019

TL&DR : ข้ามไปยังส่วนที่ II เพื่อดูของจริง

ส่วนที่ 1

@Nina Scholz คำตอบสำหรับปัญหาพื้นฐานนี้เพียงแสดงให้เราเห็นว่าอัลกอริทึมที่สวยงาม จริงๆ แล้วฉันสับสนมากด้วยเหตุผลสองประการ

  1. เมื่อฉันลอง [14,6,10,7,3] กับเป้าหมาย 500 มันจะทำการเรียก 36,783,575 ครั้งไปยังฟังก์ชัน iter โดยไม่ทำให้ call stack เสียหาย แต่หน่วยความจำกลับไม่แสดงการใช้งานที่สำคัญเลย
  2. โซลูชันการเขียนโปรแกรมแบบไดนามิกของฉันทำงานเร็วขึ้นเล็กน้อย (หรืออาจจะไม่ใช่) แต่ไม่มีวิธีใดที่สามารถทำได้ข้างต้นโดยไม่ใช้หน่วยความจำ 16GB

ดังนั้นฉันจึงเก็บโซลูชันของฉันไว้และเริ่มตรวจสอบโค้ดของเธอเพิ่มเติมอีกเล็กน้อยเกี่ยวกับเครื่องมือ dev และค้นพบทั้งความสวยงามและข้อบกพร่องเล็กน้อยด้วย

ก่อนอื่นฉันเชื่อว่าแนวทางอัลกอริทึมนี้ ซึ่งรวมถึงการใช้การเรียกซ้ำอย่างชาญฉลาด อาจสมควรได้รับชื่อของมันเอง หน่วยความจำมีประสิทธิภาพมากและใช้หน่วยความจำเฉพาะสำหรับชุดผลลัพธ์ที่สร้างขึ้นเท่านั้น สแต็กจะขยายและย่อขนาดอย่างต่อเนื่องจนถึงขีดจำกัด

ปัญหาคือ ถึงแม้จะมีประสิทธิภาพมาก แต่ก็ยังมีการโทรซ้ำซ้อนจำนวนมาก เมื่อพิจารณาถึงสิ่งนั้น ด้วยการปรับเปลี่ยนเล็กน้อย การเรียก 36,783,575 ครั้งไปยัง iter สามารถลดลงเหลือ 20,254,744... เช่น 45% ซึ่งทำให้ได้โค้ดที่เร็วกว่ามาก สิ่งนี้คืออาร์เรย์อินพุตจะต้องเรียงลำดับจากน้อยไปมาก

นี่คืออัลกอริธึมของ Nina เวอร์ชันดัดแปลง (อดทนรอ.. จะใช้เวลาประมาณ 25 วินาทีในการสรุป)

function getSum(array, sum) {
    function iter(index, temp) {cnt++ // counting iter calls -- remove in production code
        var s = temp.reduce((a, b) => a + b, 0);
        sum - s >= array[index]   && iter(index, temp.concat(array[index]));
        sum - s >= array[index+1] && iter(index + 1, temp);
        s === sum                 && result.push(temp);
        return;
    }

    var result = [];
    array.sort((x,y) => x-y); // this is a very cheap operation considering the size of the inpout array should be small for reasonable output.
    iter(0, []);
    return result;
}
var cnt = 0,
    arr = [14,6,10,7,3],
    tgt = 500,
    res;
console.time("combos");
res = getSum(arr,tgt);
console.timeEnd("combos");
console.log(`source numbers are ${arr}
found ${res.length} unique ways to sum up to ${tgt}
iter function has been called ${cnt} times`);

ส่วนที่ 2

แม้ว่าฉันจะประทับใจกับประสิทธิภาพ แต่ฉันก็ไม่พอใจกับวิธีแก้ปัญหาข้างต้นโดยไม่มีเหตุผลที่ชัดเจนที่ฉันสามารถตั้งชื่อได้ วิธีการทำงานกับผลข้างเคียงและการเรียกซ้ำสองครั้งที่ยากต่อการเข้าใจและรบกวนฉันมาก

มาถึงแนวทางของฉันสำหรับคำถามนี้ สิ่งนี้มีประสิทธิภาพมากกว่าหลายเท่าเมื่อเทียบกับโซลูชันที่ยอมรับ แม้ว่าฉันจะทำงานใน JS ก็ตาม เรายังมีที่ว่างที่จะทำให้เร็วขึ้นเล็กน้อยด้วยวิธีที่จำเป็นที่น่าเกลียด

ความแตกต่างคือ;

ตัวเลขที่กำหนด: [14,6,10,7,3] ผลรวมเป้าหมาย: 500

คำตอบที่ยอมรับ:

  • จำนวนคำตอบที่เป็นไปได้: 172686
  • แก้ไขใน: 26357ms
  • จำนวนการโทรซ้ำ: 36783575

ตอบด้านล่าง

  • จำนวนคำตอบที่เป็นไปได้: 172686
  • แก้ไขได้ใน: 1,000ms
  • จำนวนการโทรซ้ำ: 542657

function items2T([n,...ns],t){cnt++ //remove cnt in production code
    var c = ~~(t/n);
    return ns.length ? Array(c+1).fill()
                                 .reduce((r,_,i) => r.concat(items2T(ns, t-n*i).map(s => Array(i).fill(n).concat(s))),[])
                     : t % n ? []
                             : [Array(c).fill(n)];
};

var cnt = 0, result;
console.time("combos");
result = items2T([14, 6, 10, 7, 3], 500)
console.timeEnd("combos");
console.log(`${result.length} many unique ways to sum up to 500
and ${cnt} recursive calls are performed`);

จุดสำคัญอีกประการหนึ่งคือ ถ้าอาร์เรย์ที่กำหนดถูกเรียงลำดับจากมากไปน้อย จำนวนการวนซ้ำจะลดลง (บางครั้งก็มาก) ทำให้เราสามารถบีบน้ำออกจากมะนาวได้มากขึ้น เปรียบเทียบด้านบนกับด้านล่างเมื่ออาร์เรย์อินพุตถูกเรียงลำดับจากมากไปน้อย

function items2T([n,...ns],t){cnt++ //remove cnt in production code
    var c = ~~(t/n);
    return ns.length ? Array(c+1).fill()
                                 .reduce((r,_,i) => r.concat(items2T(ns, t-n*i).map(s => Array(i).fill(n).concat(s))),[])
                     : t % n ? []
                             : [Array(c).fill(n)];
};

var cnt = 0, result;
console.time("combos");
result = items2T([14, 10, 7, 6, 3], 500)
console.timeEnd("combos");
console.log(`${result.length} many unique ways to sum up to 500
and ${cnt} recursive calls are performed`);

person Redu    schedule 27.03.2019