วิธีการหลีกเลี่ยงการล้นใน expr เอบีซีดี


161

ฉันต้องคำนวณนิพจน์ที่มีลักษณะดังนี้: A*B - C*D, ชนิดของพวกเขาคือ: signed long long int A, B, C, D; แต่ละหมายเลขสามารถมีขนาดใหญ่มาก (ไม่ล้นประเภท) ในขณะที่A*Bอาจทำให้เกิดการล้นในเวลาเดียวกันการแสดงออกA*B - C*Dอาจมีขนาดเล็กมาก ฉันจะคำนวณอย่างถูกต้องได้อย่างไร

ตัวอย่างเช่น: MAX * MAX - (MAX - 1) * (MAX + 1) == 1, ที่ไหนMAX = LLONG_MAX - nและ n - จำนวนธรรมชาติบางอย่าง


17
ความแม่นยำมีความสำคัญขนาดไหน?
Anirudh Ramanathan

1
@Cululhu คำถามที่ดี เขาสามารถลองสร้างฟังก์ชั่นที่เทียบเท่าโดยใช้จำนวนที่น้อยลงโดยหารพวกมันทั้งหมดด้วย 10 หรือบางอย่างจากนั้นคูณผลลัพธ์
Chris

4
Vars A, B, C, D ลงนามแล้ว สิ่งนี้มีความหมายว่าA - Cอาจล้น มันเป็นปัญหาที่ต้องพิจารณาหรือคุณรู้ไหมว่าสิ่งนี้จะไม่เกิดขึ้นกับข้อมูลของคุณ?
วิลเลียมมอร์ริส

2
@MooingDuck แต่คุณสามารถตรวจสอบก่อนว่าการดำเนินการจะล้นstackoverflow.com/a/3224630/158285
bradgonesurfing

1
@Chris: ไม่ฉันกำลังบอกว่าไม่มีวิธีพกพาเพื่อตรวจสอบว่ามีการลงชื่อล้นเกิดขึ้น (แบรดถูกต้องที่คุณสามารถตรวจจับได้ว่าจะเกิดขึ้นได้) การใช้แอสเซมบลีแบบอินไลน์เป็นหนึ่งในวิธีที่ไม่พกพาได้หลายวิธีในการตรวจสอบ
Mooing Duck

คำตอบ:


120

ดูเหมือนว่าฉันจะเดาเรื่องนี้ไม่สำคัญ แต่A*Bเป็นสิ่งที่สามารถล้นได้

คุณสามารถทำสิ่งต่อไปนี้โดยไม่สูญเสียความแม่นยำ

A*B - C*D = A(D+E) - (A+F)D
          = AD + AE - AD - DF
          = AE - DF
             ^smaller quantities E & F

E = B - D (hence, far smaller than B)
F = C - A (hence, far smaller than C)

การสลายตัวนี้สามารถทำต่อไป
@Gian ชี้ให้เห็นว่าอาจต้องใช้ความระมัดระวังในระหว่างการดำเนินการลบหากประเภทนั้นไม่ได้ลงนามเป็นเวลานาน


ตัวอย่างเช่นในกรณีที่คุณมีคำถามจะใช้เวลาหนึ่งซ้ำ

 MAX * MAX - (MAX - 1) * (MAX + 1)
  A     B       C           D

E = B - D = -1
F = C - A = -1

AE - DF = {MAX * -1} - {(MAX + 1) * -1} = -MAX + MAX + 1 = 1

4
@Caleb เพียงใช้อัลกอริทึมเดียวกันกับC*D
Chris

2
ฉันคิดว่าคุณควรอธิบายว่า E หมายถึงอะไร
แม็กเคเล็บ

7
ทั้งยาวและยาวเป็น 64 บิต เนื่องจาก double ต้องจัดสรรบิตบางส่วนสำหรับเลขชี้กำลังมันจึงมีช่วงของค่าที่น้อยกว่าโดยไม่สูญเสียความแม่นยำ
Jim Garrison

3
@Cthulhu - ดูเหมือนว่าฉันจะใช้งานได้ก็ต่อเมื่อจำนวนทั้งหมดมีขนาดใหญ่มาก ... ตัวอย่างเช่นคุณยังคงล้นด้วย {A, B, C, D} = {MAX, MAX, MAX, 2} OP ระบุว่า "แต่ละหมายเลขสามารถใหญ่มาก" แต่มันไม่ชัดเจนจากคำแถลงปัญหาว่าแต่ละหมายเลขต้องใหญ่มาก
เควินเค

4
จะเป็นอย่างไรหากมีสิ่งใดในA,B,C,Dแง่ลบ จะไม่EหรือFใหญ่กว่านี้อีกแล้วเหรอ?
Supr

68

ทางออกที่ง่ายที่สุดและทั่วไปที่สุดคือการใช้การแสดงที่ไม่สามารถล้นได้โดยการใช้ไลบรารี่จำนวนเต็ม (เช่น http://gmplib.org/ ) หรือใช้แทน struct หรืออาเรย์ คือการแยกแต่ละหมายเลขออกเป็นสองส่วน 32 บิตและดำเนินการคูณดังนี้

(R1 + R2 * 2^32 + R3 * 2^64 + R4 * 2^96) = R = A*B = (A1 + A2 * 2^32) * (B1 + B2 * 2^32) 
R1 = (A1*B1) % 2^32
R2 = ((A1*B1) / 2^32 + (A1*B2) % 2^32 + (A2*B1) % 2^32) % 2^32
R3 = (((A1*B1) / 2^32 + (A1*B2) % 2^32 + (A2*B1) % 2^32) / 2^32 + (A1*B2) / 2^32 + (A2*B1) / 2^32 + (A2*B2) % 2^32) %2^32
R4 = ((((A1*B1) / 2^32 + (A1*B2) % 2^32 + (A2*B1) % 2^32) / 2^32 + (A1*B2) / 2^32 + (A2*B1) / 2^32 + (A2*B2) % 2^32) / 2^32) + (A2*B2) / 2^32

สมมติว่าผลลัพธ์สุดท้ายพอดีใน 64 บิตคุณจริง ๆ แล้วไม่ต้องการบิตส่วนใหญ่ของ R3 และไม่มี R4


8
การคำนวณข้างต้นนั้นไม่ซับซ้อนอย่างที่เห็นจริง ๆ แล้วมันเป็นการคูณยาวอย่างง่ายในฐาน 2 ^ 32 และรหัสใน C ควรดูง่ายขึ้น นอกจากนี้ยังเป็นความคิดที่ดีที่จะสร้างฟังก์ชั่นทั่วไปเพื่อทำงานนี้ในโปรแกรมของคุณ
Ofir

46

โปรดทราบว่านี่ไม่ได้เป็นมาตรฐานเนื่องจากอาศัยการล้อมรอบการลงชื่อเข้าใช้แบบปิดล้อม (GCC มีสถานะคอมไพเลอร์ซึ่งเปิดใช้งานสิ่งนี้)

แต่ถ้าคุณเพียงแค่ทำคำนวณทั้งหมดในlong longผลของการใช้สูตรโดยตรง: จะมีความแม่นยำตราบใดที่เหมาะกับผลที่ถูกต้องเป็น
(A * B - C * D)long long


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

(long long)((unsigned long long)A * B - (unsigned long long)C * D)

สิ่งนี้ปลดเปลื้องอินพุตไปยังunsigned long longที่ซึ่งมีการรับประกันการทำงานแบบโอเวอร์โฟลว์ที่จะถูกล้อมรอบด้วยมาตรฐาน การส่งกลับไปยังจำนวนเต็มที่มีลายเซ็นในตอนท้ายเป็นส่วนที่กำหนดการนำไปใช้ แต่จะใช้ได้กับทุกสภาพแวดล้อมในปัจจุบัน


หากคุณต้องการวิธีแก้ปัญหาที่มากขึ้นฉันคิดว่าคุณต้องใช้ "คณิตศาสตร์ยาว"


+1 คุณเป็นคนเดียวที่จะสังเกตเห็นสิ่งนี้ long longส่วนที่ยุ่งยากเพียงคือการตั้งค่าคอมไพเลอร์ที่จะทำห่อรอบล้นและการตรวจสอบถ้าผลที่ถูกต้องจริงไม่พอดี
Mysticial

2
แม้แต่รุ่นที่ไร้เดียงสาที่ไม่มีลูกเล่นใด ๆ ก็ตามก็ยังสามารถทำสิ่งที่ถูกต้องในการใช้งานส่วนใหญ่ มันไม่ได้รับประกันตามมาตรฐาน แต่คุณจะต้องหาเครื่องเสริม 1 ตัวหรืออุปกรณ์แปลก ๆ อื่น ๆ เพื่อให้มันล้มเหลว
ฮอบส์

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

18

สิ่งนี้ควรใช้งานได้ (ฉันคิดว่า):

signed long long int a = 0x7ffffffffffffffd;
signed long long int b = 0x7ffffffffffffffd;
signed long long int c = 0x7ffffffffffffffc;
signed long long int d = 0x7ffffffffffffffe;
signed long long int bd = b / d;
signed long long int bdmod = b % d;
signed long long int ca = c / a;
signed long long int camod = c % a;
signed long long int x = (bd - ca) * a * d - (camod * d - bdmod * a);

นี่คือที่มาของฉัน:

x = a * b - c * d
x / (a * d) = (a * b - c * d) / (a * d)
x / (a * d) = b / d - c / a

now, the integer/mod stuff:
x / (a * d) = (b / d + ( b % d ) / d) - (c / a + ( c % a ) / a )
x / (a * d) = (b / d - c / a) - ( ( c % a ) / a - ( b % d ) / d)
x = (b / d - c / a) * a * d - ( ( c % a ) * d - ( b % d ) * a)

1
ขอบคุณ @ bradgonesurfing - คุณให้ข้อมูลเช่นนี้ได้หรือไม่? ฉันได้อัปเดตคำตอบของฉันดำเนินการแล้วและใช้งานได้ (bd และ ca เป็น 0) ...
paquetp

1
อืมม ตอนนี้ฉันคิดว่ามันอาจจะไม่ กรณีเลวลงด้วย d = 1 และ a = 1 และ b = maxint และ c = maxint มันยังคงทำงาน Cool :)
bradgonesurfing

1
@ paquetp: a = 1, b = 0x7fffffffffffffffff, c = -0x7fffffffffffffffffff, d = 1 (note c เป็นค่าลบ) ชาญฉลาด แต่ฉันมั่นใจว่ารหัสของคุณจะจัดการกับจำนวนบวกทั้งหมดได้อย่างถูกต้อง
Mooing Duck

3
@MooingDuck แต่คำตอบสุดท้ายสำหรับชุดของคุณโอเวอร์โฟลว์เช่นกันดังนั้นจึงไม่ใช่การตั้งค่าที่ถูกต้อง จะใช้งานได้หากแต่ละด้านมีสัญลักษณ์เดียวกันดังนั้นการลบที่เกิดขึ้นจึงอยู่ในช่วง
bradgonesurfing

1
มีบางสิ่งที่แปลกกับ StackOverflow เมื่อคำตอบนี้ง่ายที่สุดและดีที่สุดได้คะแนนต่ำเมื่อเทียบกับคำตอบที่ได้คะแนนสูงสุด
bradgonesurfing

9

คุณสามารถพิจารณาการคำนวณปัจจัยร่วมที่ยิ่งใหญ่ที่สุดสำหรับค่าทั้งหมดของคุณแล้วหารด้วยปัจจัยนั้นก่อนทำการคำนวณทางคณิตศาสตร์ของคุณจากนั้นคูณอีกครั้ง นี้อนุมานว่าปัจจัยดังกล่าวอยู่อย่างไร (ตัวอย่างเช่นถ้าA, B, CและDเกิดขึ้นเป็นความสำคัญที่พวกเขาจะไม่ได้มีปัจจัยร่วมกัน)

ในทำนองเดียวกันคุณอาจลองพิจารณาเครื่องชั่งน้ำหนัก แต่สิ่งนี้น่ากลัวเล็กน้อยโดยขึ้นอยู่กับความแม่นยำเชิงตัวเลข


1
ลอการิทึมดูเหมือนจะดีถ้าlong doubleมี ในกรณีดังกล่าวสามารถบรรลุระดับความแม่นยำที่ยอมรับได้ (และผลลัพธ์จะถูกปัดเศษ)

9

หากผลลัพธ์นั้นพอดีกับ int ที่มีความยาวมากดังนั้นนิพจน์ A * BC * D ก็โอเคเมื่อทำการคำนวณทางคณิตศาสตร์ mod 2 ^ 64 และจะให้ผลลัพธ์ที่ถูกต้อง ปัญหาคือการรู้ว่าผลลัพธ์ที่ได้จะเหมาะกับ int ที่มีความยาวมากหรือไม่ ในการตรวจจับสิ่งนี้คุณสามารถใช้เคล็ดลับต่อไปนี้โดยใช้คู่:

if( abs( (double)A*B - (double)C*D ) > MAX_LLONG ) 
    Overflow
else 
    return A*B-C*D;

ปัญหาของวิธีนี้คือคุณถูก จำกัด ด้วยความแม่นยำของ mantissa ของ double (54bits?) ดังนั้นคุณจำเป็นต้อง จำกัด ผลิตภัณฑ์ A * B และ C * D ถึง 63 + 54 บิต (หรืออาจน้อยกว่านี้เล็กน้อย)


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

1
ดีและสง่างาม! คุณไม่ได้ตกหลุมพรางที่คนอื่นล้ม อีกสิ่งหนึ่ง: ฉันจะเดิมพันมีตัวอย่างที่การคำนวณสองครั้งต่ำกว่า MAX_LLONG เพียงเนื่องจากข้อผิดพลาดในการปัดเศษ สัญชาตญาณทางคณิตศาสตร์ของฉันบอกฉันว่าคุณควรคำนวณความแตกต่างของผลลัพธ์สองเท่าและความยาวแทนและเปรียบเทียบกับ MAX_LLONG / 2 หรือบางอย่าง ความแตกต่างนี้เป็นข้อผิดพลาดในการปัดเศษของการคำนวณสองครั้งและบวกการล้นและโดยปกติควรจะค่อนข้างต่ำ แต่ในกรณีที่ฉันกล่าวถึงมันจะมีขนาดใหญ่ แต่ตอนนี้ฉันขี้เกียจเกินกว่าจะหาคำตอบได้อย่างแน่นอน :-)
Hans-Peter Störr

9
E = max(A,B,C,D)
A1 = A -E;
B1 = B -E;
C1 = C -E;
D1 = D -E;

แล้วก็

A*B - C*D = (A1+E)*(B1+E)-(C1+E)(D1+E) = (A1+B1-C1-D1)*E + A1*B1 -C1*D1

7

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

จำนวน123สามารถแสดงเป็น:

123 = 100 * 1 + 10 * 2 + 3

ที่คุณเพิ่งสร้างอาร์เรย์ [1 2 3]ที่คุณเพียงแค่สร้างอาร์เรย์

คุณทำสิ่งนี้กับตัวเลข A, B, C และ D ทั้งหมดจากนั้นคุณคูณมันเป็นพหุนาม เมื่อคุณได้พหุนามที่เกิดขึ้นคุณก็จะสร้างจำนวนขึ้นมาใหม่


2
ไม่รู้ว่าเป็นอะไร แต่ฉันจะต้องค้นหา ใส่ :) นี่เป็นวิธีการแก้ปัญหาของด้านบนของหัวของฉันขณะที่ฉันกำลังช้อปปิ้งกับแฟนของฉัน :)
หมดเวลา

คุณกำลังใช้ bignums ในอาร์เรย์ base10 GMP เป็นห้องสมุด bignum คุณภาพที่ใช้ฐาน 4294967296 เร็วขึ้น ไม่มี downvote เนื่องจากคำตอบนั้นถูกต้องและมีประโยชน์
Mooing Duck

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

อย่างไรก็ตาม ... การใช้โซลูชันนี้คุณสามารถใช้คอมพิวเตอร์จำนวนที่มากกว่าตัวอักษรแบบดั้งเดิมได้ (เช่น 100 หลัก) และทำให้ผลลัพธ์เป็นอาร์เรย์ นี่สมควรได้รับการโหวตเพิ่มขึ้น: p
Mihai

ฉันไม่แน่ใจว่าจะได้รับ upvote เนื่องจากวิธีนี้ (แม้ว่าจะมีประสิทธิภาพและค่อนข้างง่ายต่อการเข้าใจ) เป็นหน่วยความจำที่หิวและช้า
Mooing Duck

6

ในขณะที่signed long long intจะไม่ถือA*Bสองคนจะ ดังนั้นอาจจะย่อยสลายไปตามข้อกำหนดของต้นไม้สัญลักษณ์ที่แตกต่างกันของพวกเขาเหมาะสมอย่างใดอย่างหนึ่งA*Bsigned long long int

A1=A>>32;
A0=A & 0xffffffff;
B1=B>>32;
B0=B & 0xffffffff;

AB_0=A0*B0;
AB_1=A0*B1+A1*B0;
AB_2=A1*B1;

C*Dสำหรับเดียวกัน

เมื่อถึงทางตรงการสับเปลี่ยนสามารถทำได้กับทุกคู่AB_iและCD_iเช่นเดียวกันโดยใช้บิตพกพาเพิ่มเติม (แม่นยำเป็นจำนวนเต็ม 1 บิต) สำหรับแต่ละ ดังนั้นถ้าเราบอกว่า E = A * BC * D คุณจะได้รับ:

E_00=AB_0-CD_0 
E_01=(AB_0 > CD_0) == (AB_0 - CD_0 < 0) ? 0 : 1  // carry bit if overflow
E_10=AB_1-CD_1 
...

เราดำเนินการต่อโดยการโอนครึ่งบนของE_10เป็นE_20(เลื่อน 32 และเพิ่มจากนั้นลบครึ่งบนของE_10)

ตอนนี้คุณสามารถกำจัดบิตพกE_11โดยการเพิ่มด้วยการเข้าสู่ระบบที่ถูกต้อง (ที่ได้รับจากส่วนที่ไม่ได้พกพา) E_20เพื่อ หากสิ่งนี้ทริกเกอร์โอเวอร์โฟลว์ผลลัพธ์จะไม่พอดี

E_10ขณะนี้มีเพียงพอพื้นที่ 'จะใช้เวลาครึ่งบนจากE_00 (กะเพิ่มลบ) E_01และบิตพก

E_10E_20อาจมีขนาดใหญ่ในขณะนี้อีกครั้งเพื่อให้เราทำซ้ำถ่ายโอนไปยัง

ณ จุดนี้E_20จะต้องกลายเป็นศูนย์มิฉะนั้นผลลัพธ์จะไม่พอดี ครึ่งบนของE_10ว่างเปล่าเป็นผลมาจากการถ่ายโอนเกินไป

ขั้นตอนสุดท้ายคือการถ่ายโอนครึ่งล่างของE_20เข้าไปE_10อีกครั้ง

หากความคาดหวังที่E=A*B+C*Dเหมาะสมกับการsigned long long intถือครองตอนนี้เรามี

E_20=0
E_10=0
E_00=E

1
นี่เป็นสูตรง่ายที่เราจะได้รับหากใช้สูตรคูณของ Ofir และลบผลลัพธ์ชั่วคราวที่ไร้ประโยชน์ทั้งหมด
dronus

3

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

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

การใช้งานต่อไปนี้signed charและunsigned charเพื่อสาธิตรหัส สำหรับการดำเนินงานของคุณเปลี่ยนความหมายของSignedการtypedef signed long long int Signed;และความหมายของการUnsignedtypedef unsigned long long int Unsigned;

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>


//  Define the signed and unsigned types we wish to use.
typedef signed char   Signed;
typedef unsigned char Unsigned;

//  uHalfModulus is half the modulus of the unsigned type.
static const Unsigned uHalfModulus = UCHAR_MAX/2+1;

//  sHalfModulus is the negation of half the modulus of the unsigned type.
static const Signed   sHalfModulus = -1 - (Signed) (UCHAR_MAX/2);


/*  Map the unsigned value to the signed value that is the same modulo the
    modulus of the unsigned type.  If the input x maps to a positive value, we
    simply return x.  If it maps to a negative value, we return x minus the
    modulus of the unsigned type.

    In most C implementations, this routine could simply be "return x;".
    However, this version uses several steps to convert x to a negative value
    so that overflow is avoided.
*/
static Signed ConvertToSigned(Unsigned x)
{
    /*  If x is representable in the signed type, return it.  (In some
        implementations, 
    */
    if (x < uHalfModulus)
        return x;

    /*  Otherwise, return x minus the modulus of the unsigned type, taking
        care not to overflow the signed type.
    */
    return (Signed) (x - uHalfModulus) - sHalfModulus;
}


/*  Calculate A*B - C*D given that the result is representable as a Signed
    value.
*/
static signed char Calculate(Signed A, Signed B, Signed C, Signed D)
{
    /*  Map signed values to unsigned values.  Positive values are unaltered.
        Negative values have the modulus of the unsigned type added.  Because
        we do modulo arithmetic below, adding the modulus does not change the
        final result.
    */
    Unsigned a = A;
    Unsigned b = B;
    Unsigned c = C;
    Unsigned d = D;

    //  Calculate with modulo arithmetic.
    Unsigned t = a*b - c*d;

    //  Map the unsigned value to the corresponding signed value.
    return ConvertToSigned(t);
}


int main()
{
    //  Test every combination of inputs for signed char.
    for (int A = SCHAR_MIN; A <= SCHAR_MAX; ++A)
    for (int B = SCHAR_MIN; B <= SCHAR_MAX; ++B)
    for (int C = SCHAR_MIN; C <= SCHAR_MAX; ++C)
    for (int D = SCHAR_MIN; D <= SCHAR_MAX; ++D)
    {
        //  Use int to calculate the expected result.
        int t0 = A*B - C*D;

        //  If the result is not representable in signed char, skip this case.
        if (t0 < SCHAR_MIN || SCHAR_MAX < t0)
            continue;

        //  Calculate the result with the sample code.
        int t1 = Calculate(A, B, C, D);

        //  Test the result for errors.
        if (t0 != t1)
        {
            printf("%d*%d - %d*%d = %d, but %d was returned.\n",
                A, B, C, D, t0, t1);
            exit(EXIT_FAILURE);
        }
    }
    return 0;
}

2

คุณสามารถลองแบ่งสมการออกเป็นส่วนย่อย ๆ ซึ่งไม่ล้น

AB - CD
= [ A(B - N) - C( D - M )] + [AN - CM]

= ( AK - CJ ) + ( AN - CM)

    where K = B - N
          J = D - M

หากส่วนประกอบยังคงล้นอยู่คุณสามารถแบ่งให้เป็นส่วนประกอบที่เล็กลงซ้ำแล้วรวมเข้าด้วยกันอีกครั้ง


สิ่งนี้อาจหรืออาจไม่ถูกต้อง แต่สร้างความสับสนอย่างแน่นอน คุณกำหนดKและJทำไมไม่และN Mนอกจากนี้ฉันคิดว่าคุณกำลังแบ่งสมการออกเป็นชิ้นใหญ่ เนื่องจากขั้นตอนที่ 3 ของคุณเหมือนกับคำถามของ OP ยกเว้นเรื่องที่ซับซ้อนมากขึ้น(AK-CJ)->(AB-CD)
Mooing Duck

N ไม่ง่ายจากสิ่งใด มันแค่ลบจำนวนออกจาก A เพื่อทำให้มันเล็กลง จริงๆแล้วมันเป็นวิธีการแก้ปัญหาที่คล้ายกัน แต่ด้อยกว่าการ paquetp ที่นี่ฉันใช้การลบแทนการหารจำนวนเต็มเพื่อทำให้เล็กลง
bradgonesurfing

2

ฉันอาจไม่ได้ครอบคลุมกรณีขอบทั้งหมดหรือฉันไม่ได้ทดสอบอย่างจริงจัง แต่นี่เป็นเทคนิคที่ฉันจำได้ว่าใช้ในยุค 80 เมื่อพยายามทำเลขจำนวนเต็ม 32 บิตในซีพียู 16 บิต โดยพื้นฐานแล้วคุณแบ่ง 32 บิตออกเป็นสองหน่วย 16 บิตและทำงานแยกกัน

public class DoubleMaths {
  private static class SplitLong {
    // High half (or integral part).
    private final long h;
    // Low half.
    private final long l;
    // Split.
    private static final int SPLIT = (Long.SIZE / 2);

    // Make from an existing pair.
    private SplitLong(long h, long l) {
      // Let l overflow into h.
      this.h = h + (l >> SPLIT);
      this.l = l % (1l << SPLIT);
    }

    public SplitLong(long v) {
      h = v >> SPLIT;
      l = v % (1l << SPLIT);
    }

    public long longValue() {
      return (h << SPLIT) + l;
    }

    public SplitLong add ( SplitLong b ) {
      // TODO: Check for overflow.
      return new SplitLong ( longValue() + b.longValue() );
    }

    public SplitLong sub ( SplitLong b ) {
      // TODO: Check for overflow.
      return new SplitLong ( longValue() - b.longValue() );
    }

    public SplitLong mul ( SplitLong b ) {
      /*
       * e.g. 10 * 15 = 150
       * 
       * Divide 10 and 15 by 5
       * 
       * 2 * 3 = 5
       * 
       * Must therefore multiply up by 5 * 5 = 25
       * 
       * 5 * 25 = 150
       */
      long lbl = l * b.l;
      long hbh = h * b.h;
      long lbh = l * b.h;
      long hbl = h * b.l;
      return new SplitLong ( lbh + hbl, lbl + hbh );
    }

    @Override
    public String toString () {
      return Long.toHexString(h)+"|"+Long.toHexString(l);
    }
  }

  // I'll use long and int but this can apply just as easily to long-long and long.
  // The aim is to calculate A*B - C*D without overflow.
  static final long A = Long.MAX_VALUE;
  static final long B = Long.MAX_VALUE - 1;
  static final long C = Long.MAX_VALUE;
  static final long D = Long.MAX_VALUE - 2;

  public static void main(String[] args) throws InterruptedException {
    // First do it with BigIntegers to get what the result should be.
    BigInteger a = BigInteger.valueOf(A);
    BigInteger b = BigInteger.valueOf(B);
    BigInteger c = BigInteger.valueOf(C);
    BigInteger d = BigInteger.valueOf(D);
    BigInteger answer = a.multiply(b).subtract(c.multiply(d));
    System.out.println("A*B - C*D = "+answer+" = "+answer.toString(16));

    // Make one and test its integrity.
    SplitLong sla = new SplitLong(A);
    System.out.println("A="+Long.toHexString(A)+" ("+sla.toString()+") = "+Long.toHexString(sla.longValue()));

    // Start small.
    SplitLong sl10 = new SplitLong(10);
    SplitLong sl15 = new SplitLong(15);
    SplitLong sl150 = sl10.mul(sl15);
    System.out.println("10="+sl10.longValue()+"("+sl10.toString()+") * 15="+sl15.longValue()+"("+sl15.toString()+") = "+sl150.longValue() + " ("+sl150.toString()+")");

    // The real thing.
    SplitLong slb = new SplitLong(B);
    SplitLong slc = new SplitLong(C);
    SplitLong sld = new SplitLong(D);
    System.out.println("B="+Long.toHexString(B)+" ("+slb.toString()+") = "+Long.toHexString(slb.longValue()));
    System.out.println("C="+Long.toHexString(C)+" ("+slc.toString()+") = "+Long.toHexString(slc.longValue()));
    System.out.println("D="+Long.toHexString(D)+" ("+sld.toString()+") = "+Long.toHexString(sld.longValue()));
    SplitLong sanswer = sla.mul(slb).sub(slc.mul(sld));
    System.out.println("A*B - C*D = "+sanswer+" = "+sanswer.longValue());

  }

}

พิมพ์:

A*B - C*D = 9223372036854775807 = 7fffffffffffffff
A=7fffffffffffffff (7fffffff|ffffffff) = 7fffffffffffffff
10=10(0|a) * 15=15(0|f) = 150 (0|96)
B=7ffffffffffffffe (7fffffff|fffffffe) = 7ffffffffffffffe
C=7fffffffffffffff (7fffffff|ffffffff) = 7fffffffffffffff
D=7ffffffffffffffd (7fffffff|fffffffd) = 7ffffffffffffffd
A*B - C*D = 7fffffff|ffffffff = 9223372036854775807

ซึ่งดูเหมือนว่าฉันจะทำงาน

ฉันพนันว่าฉันพลาดรายละเอียดปลีกย่อยบางอย่างเช่นการเฝ้าดูเครื่องหมายล้นเป็นต้น แต่ฉันคิดว่าสิ่งสำคัญอยู่ตรงนั้น


1
ฉันคิดว่านี่เป็นการใช้งานสิ่งที่ @Ofir แนะนำ
OldCurmudgeon

2

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

ดังนั้นวิธีง่ายๆอาจเป็น:

(long long)((__int128)A * B - (__int128)C * D)

1

AB-CD = (AB-CD) * AC / AC = (B/C-D/A)*A*C. ทั้งB/Cมิได้D/Aสามารถล้นดังนั้นการคำนวณ(B/C-D/A)ครั้งแรก เนื่องจากผลลัพธ์สุดท้ายจะไม่ล้นตามคำจำกัดความของคุณคุณสามารถดำเนินการคูณและคำนวณได้อย่างปลอดภัย(B/C-D/A)*A*Cว่าเป็นผลลัพธ์ที่ต้องการหรือไม่

หมายเหตุหากอินพุตของคุณมีขนาดเล็กมากเช่นกันB/CหรือD/Aล้นได้ หากเป็นไปได้อาจต้องใช้การจัดการที่ซับซ้อนมากขึ้นตามการตรวจสอบอินพุต


2
ที่จะไม่ทำงานเป็นส่วนจำนวนเต็มสูญเสียข้อมูล (เศษของผล)
Ofir

@Ofir ถูกต้อง แต่คุณไม่สามารถกินเค้กและปล่อยให้มันไม่ถูกแตะต้อง คุณต้องจ่ายด้วยความแม่นยำหรือใช้ทรัพยากรเพิ่มเติม (เช่นที่คุณแนะนำในคำตอบ) คำตอบของฉันมีลักษณะทางคณิตศาสตร์ในขณะที่คุณใช้คอมพิวเตอร์ แต่ละคนสามารถแก้ไขได้ขึ้นอยู่กับสถานการณ์
SomeWittyUsername

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

@Ofir ฉันไม่คิดว่าภาษาของคุณไม่เหมาะสม OP ต้องการการคำนวณที่ "ถูกต้อง" อย่างชัดเจนไม่ใช่ว่าจะสูญเสียความแม่นยำในการดำเนินการภายใต้ข้อ จำกัด ของทรัพยากรอย่างมาก
user4815162342

1

เลือกK = a big number(เช่นK = A - sqrt(A))

A*B - C*D = (A-K)*(B-K) - (C-K)*(D-K) + K*(A-C+B-D); // Avoid overflow.

ทำไม?

(A-K)*(B-K) = A*B - K*(A+B) + K^2
(C-K)*(D-K) = C*D - K*(C+D) + K^2

=>
(A-K)*(B-K) - (C-K)*(D-K) = A*B - K*(A+B) + K^2 - {C*D - K*(C+D) + K^2}
(A-K)*(B-K) - (C-K)*(D-K) = A*B - C*D - K*(A+B) + K*(C+D) + K^2 - K^2
(A-K)*(B-K) - (C-K)*(D-K) = A*B - C*D - K*(A+B-C-D)

=>
A*B - C*D = (A-K)*(B-K) - (C-K)*(D-K) + K*(A+B-C-D)

=>
A*B - C*D = (A-K)*(B-K) - (C-K)*(D-K) + K*(A-C+B-D)

โปรดทราบว่าเนื่องจาก A, B, C และ D เป็นจำนวนมากดังนั้นA-CและB-Dมีจำนวนน้อย


คุณเลือกKในทางปฏิบัติได้อย่างไร นอกจากนี้K * (A-C + BD)อาจยังล้นอยู่
ylc

@ylc: เลือก K = sqrt (A) ไม่ใช่A-C+B-Dขนาดเล็ก เนื่องจาก A, B, C และ D เป็นจำนวนมากดังนั้น AC จึงมีจำนวนน้อย
Amir Saniyan

หากคุณเลือกK = sqrt (A)ดังนั้น(AK) * (BK)อาจล้นได้อีกครั้ง
ylc

@ylc: ตกลง! ฉันเปลี่ยนเป็นA - sqrt(A):)
Amir Saniyan

ดังนั้นK * (A-C + BD)อาจล้น
ylc
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.