อัลกอริทึมใดที่แม่นยำยิ่งขึ้นสำหรับการคำนวณผลรวมของอาร์เรย์ที่เรียงลำดับตัวเลข?


22

ป.ร. ให้ไว้เป็นลำดับ จำกัด ที่เพิ่มขึ้นของตัวเลขบวก{n} อัลกอริทึมสองข้อใดต่อไปนี้ที่ดีกว่าสำหรับการคำนวณผลรวมของตัวเลขZ1,Z2,.....Zn

s=0; 
for \ i=1:n 
    s=s + z_{i} ; 
end

หรือ:

s=0; 
for \ i=1:n 
s=s + z_{n-i+1} ; 
end

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

ถูกต้องหรือไม่ อะไรที่สามารถพูดได้?

คำตอบ:


18

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

แต่คุณจะได้ผลลัพธ์ที่ดีกว่า (และทำงานได้เร็วขึ้น) หากคุณสร้างผลรวมสี่ตัวอย่าง: เริ่มต้นด้วย sum1, sum2, sum3, sum4 และเพิ่มองค์ประกอบอาเรย์สี่ตัวกลับไปที่ sum1, sum2, sum3, sum4 เนื่องจากผลลัพธ์แต่ละรายการโดยเฉลี่ยเพียง 1 ใน 4 ของผลรวมดั้งเดิมข้อผิดพลาดของคุณจึงเล็กลงสี่เท่า

ยังดีกว่า: เพิ่มตัวเลขเป็นคู่ จากนั้นเพิ่มผลลัพธ์เป็นคู่ เพิ่มผลลัพธ์เหล่านั้นเป็นคู่อีกครั้งและต่อ ๆ ไปจนกระทั่งคุณเหลือตัวเลขสองตัวที่จะเพิ่ม

ง่ายมาก: ใช้ความแม่นยำสูงกว่า ใช้ long double เพื่อคำนวณผลรวมของ double ใช้สองเท่าเพื่อคำนวณผลรวมของจำนวนลอย

ใกล้ถึงสมบูรณ์แบบ: ค้นหาอัลกอริทึมของ Kahan ซึ่งอธิบายไว้ก่อนหน้านี้ ยังคงใช้ดีที่สุดโดยการเพิ่มเริ่มต้นด้วยจำนวนที่น้อยที่สุด


26

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


24

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

สมมติว่าเรากำลังทำงานในรูปแบบจุดลอยที่ให้ความแม่นยำ 3 หลัก ตอนนี้เราต้องการเพิ่มหมายเลขสิบ:

[1000, 1, 1, 1, 1, 1, 1, 1, 1, 1]

แน่นอนคำตอบที่แน่นอนคือ 1,059 แต่เราไม่สามารถรับได้ในรูปแบบ 3 หลักของเรา การปัดเศษเป็นตัวเลข 3 หลักคำตอบที่แม่นยำที่สุดที่เราได้รับคือ 1,010 ถ้าเราเพิ่มเล็กที่สุดไปหามากที่สุดในแต่ละวงเราจะได้รับ:

Loop Index        s
1                 1
2                 2
3                 3
4                 4
5                 5
6                 6
7                 7
8                 8
9                 9
10                1009 -> 1010

ดังนั้นเราจึงได้คำตอบที่แม่นยำที่สุดสำหรับรูปแบบของเรา ทีนี้สมมติว่าเราเพิ่มจากมากไปหาน้อย

Loop Index        s
1                 1000
2                 1001 -> 1000
3                 1001 -> 1000
4                 1001 -> 1000
5                 1001 -> 1000
6                 1001 -> 1000
7                 1001 -> 1000
8                 1001 -> 1000
9                 1001 -> 1000
10                1001 -> 1000

เนื่องจากจำนวนจุดลอยตัวจะถูกปัดเศษหลังจากการดำเนินการแต่ละครั้งส่วนที่เหลือทั้งหมดจะถูกปัดเศษทำให้ข้อผิดพลาดของเราเพิ่มขึ้นจาก 1 เป็น 9 จากที่แน่นอน ทีนี้ลองคิดดูว่าชุดตัวเลขที่คุณต้องการเพิ่มมี 1,000 จากนั้นเป็นร้อย 1 หรือล้าน โปรดทราบว่าเพื่อให้มีความถูกต้องอย่างแท้จริงคุณจะต้องรวมตัวเลขสองตัวที่เล็กที่สุดแล้วนำผลลัพธ์มารวมกันเป็นชุดตัวเลข


15

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

สำหรับการอ้างอิงสิ่งที่โปรแกรมเมอร์ทุกคนควรรู้เกี่ยวกับเลขทศนิยมนั้นครอบคลุมประเด็นพื้นฐานอย่างละเอียดเพียงพอที่ใครบางคนสามารถอ่านได้ใน 20 นาที (+/- 10) และเข้าใจพื้นฐาน "สิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนควรรู้เกี่ยวกับเลขคณิตจุดลอยตัว" โดย Goldberg เป็นการอ้างอิงแบบคลาสสิก แต่คนส่วนใหญ่ฉันรู้ว่าใครแนะนำกระดาษที่ยังไม่ได้อ่านอย่างละเอียดเพราะมันอยู่ที่ประมาณ 50 หน้า (มากกว่านั้นในบางเล่ม สิ่งพิมพ์) และเขียนด้วยร้อยแก้วที่หนาแน่นดังนั้นฉันจึงมีปัญหาในการแนะนำว่าเป็นการอ้างอิงบรรทัดแรกสำหรับผู้คน เป็นการดีสำหรับการดูครั้งที่สองที่วัตถุ การอ้างอิงสารานุกรมคือความแม่นยำและความเสถียรของอัลกอริธึมเชิงตัวเลขของ Highamซึ่งครอบคลุมเนื้อหานี้รวมถึงการสะสมข้อผิดพลาดเชิงตัวเลขในอัลกอริทึมอื่น ๆ อีกมากมาย มันเป็น 680 หน้าดังนั้นฉันจะไม่ดูข้อมูลอ้างอิงนี้ก่อนเช่นกัน


2
เพื่อความสมบูรณ์ในหนังสือของไฮแทมคุณจะพบคำตอบของคำถามต้นฉบับในหน้า 82 : การสั่งซื้อที่เพิ่มขึ้นเป็นสิ่งที่ดีที่สุด นอกจากนี้ยังมีส่วน (4.6) ที่พูดถึงการเลือกวิธีการ
Federico Poloni

7

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

s=0; 
for \ i=1:n 
    printf("Hello World");
    s=s + z_{i} ; 
end

พอเพียงเพื่อลดความแม่นยำในการสรุป (!!) ดังนั้นควรระมัดระวังเป็นอย่างยิ่งหากคุณต้องการ printf-debug โค้ดของคุณขณะตรวจสอบความถูกต้อง

สำหรับผู้ที่สนใจบทความนี้อธิบายถึงปัญหาในการคำนวณตัวเลขที่ใช้กันอย่างแพร่หลาย (การแยกตัวประกอบอันดับ QR ของ Lapack's) ซึ่งการดีบั๊กและการวิเคราะห์นั้นยุ่งยากมากอย่างแม่นยำเนื่องจากปัญหานี้


1
เครื่องจักรที่ทันสมัยส่วนใหญ่เป็น 64- บิตและใช้ทั้งหน่วย SSE หรือ AVX แม้สำหรับการดำเนินงานสเกลาร์ หน่วยเหล่านั้นไม่รองรับเลขคณิต 80 บิตและใช้ความแม่นยำภายในเช่นเดียวกับอาร์กิวเมนต์การดำเนินการ โดยทั่วไปแล้วการใช้ x87 FPU นั้นเป็นสิ่งที่ท้อใจและผู้รวบรวม 64 บิตส่วนใหญ่ต้องการตัวเลือกพิเศษที่จะถูกบังคับให้ใช้งาน
Hristo Iliev

1
@HristoIliev ขอบคุณสำหรับความคิดเห็นที่ฉันไม่รู้จักสิ่งนี้!
Federico Poloni

4

จากตัวเลือก 2 ตัวการเพิ่มจากเล็กไปใหญ่จะทำให้เกิดข้อผิดพลาดน้อยกว่าตัวเลขแล้วเพิ่มจากใหญ่ไปเล็ก

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

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

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


2

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


0

ใช้การเพิ่มทรีแบบไบนารีคือเลือกค่าเฉลี่ยของการแจกแจง (จำนวนที่ใกล้เคียงที่สุด) เป็นรูตของทรีไบนารีและสร้างทรีไบนารีที่เรียงลำดับโดยเพิ่มค่าที่น้อยกว่าทางด้านซ้ายของกราฟและขนาดใหญ่ทางด้านขวาเป็นต้น . การเพิ่มโหนดลูกทั้งหมดของผู้ปกครองคนเดียวซ้ำในวิธีการจากล่างขึ้นบน สิ่งนี้จะมีประสิทธิภาพเมื่อข้อผิดพลาด avg เพิ่มขึ้นตามจำนวนของการรวมและในวิธีการแบบต้นไม้ไบนารีจำนวนของการรวมอยู่ในลำดับของ log n ในฐาน 2 ดังนั้นข้อผิดพลาด avg จะน้อยลง


สิ่งนี้เหมือนกับการเพิ่มคู่ที่อยู่ติดกันในอาร์เรย์เดิม (เนื่องจากเรียงลำดับแล้ว) ไม่มีเหตุผลที่จะใส่ค่าทั้งหมดลงในต้นไม้
Godric Seer

0

สิ่งที่ Hristo Iliev กล่าวไว้ข้างต้นเกี่ยวกับคอมไพเลอร์ 64- บิตที่เลือกใช้คำสั่ง SSE และ AVX เหนือ FPU (AKA NDP) เป็นจริงอย่างน้อยสำหรับ Microsoft Visual Studio 2013 อย่างไรก็ตามสำหรับการดำเนินการจุดลอยตัวความแม่นยำสองเท่าที่ฉันใช้ฉันพบ มันเร็วกว่าจริง ๆ รวมถึงในทางทฤษฎีที่แม่นยำยิ่งขึ้นเพื่อใช้ FPU หากเป็นสิ่งสำคัญสำหรับคุณฉันขอแนะนำให้ทดสอบวิธีแก้ปัญหาต่าง ๆ ก่อนที่จะเลือกวิธีสุดท้าย

เมื่อทำงานใน Java ฉันมักใช้ประเภทข้อมูล BigDecimal ที่มีความแม่นยำสูง มันง่ายเกินไปและมักจะไม่สังเกตเห็นว่าความเร็วลดลง การคำนวณฟังก์ชันยอดเยี่ยมด้วยอนุกรมที่ไม่มีที่สิ้นสุดและ sqrt โดยใช้วิธีของนิวตันอาจใช้เวลาเป็นมิลลิวินาทีหรือมากกว่า แต่ก็ทำได้และแม่นยำมาก


0

ฉันออกจากที่นี่เท่านั้น/programming//a/58006104/860099 (เมื่อคุณไปที่นั่นคลิกเพื่อ 'แสดงรหัสตัวอย่าง' และเรียกใช้โดยปุ่ม

เป็นตัวอย่าง JavaScript ที่แสดงให้เห็นอย่างชัดเจนว่าผลรวมที่เริ่มต้นจากที่ใหญ่ที่สุดให้ข้อผิดพลาดที่ใหญ่กว่า

arr=[9,.6,.1,.1,.1,.1];

sum     =             arr.reduce((a,c)=>a+c,0);  // =  9.999999999999998
sortSum = [...arr].sort().reduce((a,c)=>a+c,0);  // = 10

console.log('sum:     ',sum);
console.log('sortSum:',sortSum);

คำตอบสำหรับลิงก์อย่างเดียวนั้นไม่ได้รับการสนับสนุนในไซต์นี้ คุณช่วยอธิบายสิ่งที่มีให้ในลิงค์ได้หรือไม่?
nicoguaro

@nicoguaro ฉันอัปเดตคำตอบ - คำตอบทั้งหมดดีมาก แต่นี่เป็นตัวอย่างที่เป็นรูปธรรม
Kamil Kiełczewski
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.