ฉันจะไปรอบ ๆ พุ่มไม้สักพัก แต่ก็มีประเด็น
semigroups
คำตอบคือเชื่อมโยงทรัพย์สินของการดำเนินงานที่ลดลงไบนารี
มันค่อนข้างเป็นนามธรรม แต่การคูณเป็นตัวอย่างที่ดี ถ้าx , YและZคือบางส่วนจำนวนธรรมชาติ (หรือจำนวนเต็มหรือตัวเลขที่มีเหตุผลหรือตัวเลขจริงหรือตัวเลขที่ซับซ้อนหรือN × Nการฝึกอบรมหรือใด ๆ ของทั้งกลุ่มสิ่งที่มากขึ้น) แล้วx × Yเป็นชนิดเดียวกัน จำนวนเป็นทั้งxและy ที่ เราเริ่มต้นด้วยตัวเลขสองตัวดังนั้นมันจึงเป็นการดำเนินการแบบไบนารีและได้หนึ่งดังนั้นเราจึงลดจำนวนของจำนวนที่เรามีหนึ่งทำให้การดำเนินการลดลง และ ( x × y ) × zจะเท่ากับx × ( y ×เสมอ)z ) ซึ่งเป็นคุณสมบัติการเชื่อมโยง
(หากคุณรู้ทั้งหมดนี้แล้วคุณสามารถข้ามไปยังหัวข้อถัดไป)
อีกสองสามสิ่งที่คุณมักจะเห็นในวิทยาการคอมพิวเตอร์ที่ทำงานในลักษณะเดียวกัน:
- การเพิ่มตัวเลขชนิดใด ๆ เหล่านั้นแทนการคูณ
- การต่อสตริง (
"a"+"b"+"c"คือ"abc"คุณเริ่มต้นด้วย"ab"+"c"หรือ"a"+"bc")
- แยกสองรายการเข้าด้วยกัน
[a]++[b]++[c]เหมือนกัน[a,b,c]ทั้งจากหลังไปข้างหน้าหรือข้างหน้าไปข้างหลัง
consบนหัวและหางหากคุณคิดว่าหัวเป็นรายการเดี่ยว นั่นเป็นเพียงการต่อสองรายการ
- รับสหภาพหรือจุดตัดของเซต
- บูลีนและบูลีนหรือ
- บิต
&, |และ^
- องค์ประกอบของฟังก์ชั่น: ( f ∘ g ) ∘ h x = f ∘ ( g ∘ h ) x = f ( g ( h ( x )))
- สูงสุดและต่ำสุด
- นอกจากนี้โมดูโลพี
บางสิ่งที่ไม่:
- การลบเพราะ 1- (1-2) ≠ (1-1) -2
- x ⊕ y = tan ( x + y ) เนื่องจาก tan (π / 4 + π / 4) ไม่ได้ถูกกำหนด
- การคูณเหนือจำนวนลบเพราะ -1 × -1 ไม่ใช่จำนวนลบ
- การหารจำนวนเต็มซึ่งมีปัญหาทั้งหมดสามข้อ!
- เหตุผลไม่ใช่เพราะมันมีเพียงหนึ่งตัวถูกดำเนินการไม่ใช่สอง
int print2(int x, int y) { return printf( "%d %d\n", x, y ); }เป็นprint2( print2(x,y), z );และprint2( x, print2(y,z) );มีเอาต์พุตที่แตกต่างกัน
มันเป็นแนวคิดที่มีประโยชน์มากพอที่เราตั้งชื่อมัน ชุดที่มีการดำเนินการที่มีคุณสมบัติเหล่านี้คือกลุ่มย่อย ดังนั้นจำนวนจริงภายใต้การคูณจึงเป็น semigroup และคำถามของคุณกลายเป็นหนึ่งในวิธีการที่เป็นนามธรรมประเภทนี้จะเป็นประโยชน์ในโลกแห่งความจริง การดำเนินการของกลุ่มย่อยทั้งหมดสามารถปรับให้เหมาะกับวิธีที่คุณต้องการได้
ลองทำที่บ้าน
เท่าที่ฉันรู้เทคนิคนี้ได้รับการอธิบายเป็นครั้งแรกในปี 1974 ในเอกสารของ Daniel Friedman และ David Wise “ การเรียกซ้ำการพับแบบเทิดทูนให้กลายเป็นการวนซ้ำ”แม้ว่าพวกเขาจะสันนิษฐานว่ามีคุณสมบัติมากกว่าที่พวกเขาต้องการ
Haskell เป็นภาษาที่ยอดเยี่ยมในการอธิบายสิ่งนี้เพราะมันมีประเภทของงานSemigroupพิมพ์ในห้องสมุดมาตรฐาน มันเรียกร้องการดำเนินงานของทั่วไปผู้ประกอบการSemigroup <>เนื่องจากรายการและสตริงเป็นอินสแตนซ์ของอินสแตนซ์ของSemigroupพวกเขาจึงกำหนด<>เป็นตัวดำเนินการเรียงต่อกัน++เป็นต้น และมีการนำเข้าที่เหมาะสม[a] <> [b]เป็นนามแฝงสำหรับซึ่งเป็น[a] ++ [b][a,b]
แต่แล้วตัวเลขล่ะ เราเพียงแค่เห็นว่าประเภทที่เป็นตัวเลข semigroups ภายใต้การอย่างใดอย่างหนึ่งนอกจากหรือคูณ! เพื่อที่หนึ่งที่ได้รับจะ<>เป็นDouble? อย่างใดอย่างหนึ่ง! Haskell กำหนดประเภทProduct Double, where (<>) = (*)(นั่นคือความหมายที่เกิดขึ้นจริงใน Haskell) และยัง,Sum Doublewhere (<>) = (+)
หนึ่งรอยย่นคือคุณใช้ความจริงที่ว่า 1 คือเอกลักษณ์ของการคูณ กึ่งกลุ่มที่มีตัวตนที่เรียกว่าหนังสือและถูกกำหนดไว้ในแพคเกจ Haskell Data.Monoidซึ่งเรียกเอกลักษณ์องค์ประกอบทั่วไปของ memptytypeclass Sum, Productและรายชื่อแต่ละคนมีเอกลักษณ์องค์ประกอบ (0, 1 และ[]ตามลำดับ) เพื่อให้พวกเขามีกรณีเช่นเดียวกับMonoid Semigroup(เพื่อไม่ให้สับสนกับmonadดังนั้นเพียงลืมฉันยังนำขึ้นเหล่านั้น)
ข้อมูลเพียงพอที่จะแปลอัลกอริทึมของคุณเป็นฟังก์ชัน Haskell โดยใช้ monoids:
module StylizedRec (pow) where
import Data.Monoid as DM
pow :: Monoid a => a -> Word -> a
{- Applies the monoidal operation of the type of x, whatever that is, by
- itself n times. This is already in Haskell as Data.Monoid.mtimes, but
- let’s write it out as an example.
-}
pow _ 0 = mempty -- Special case: Return the nullary product.
pow x 1 = x -- The base case.
pow x n = x <> (pow x (n-1)) -- The recursive case.
ที่สำคัญโปรดทราบว่านี่คือ semigroup หางแบบเรียกซ้ำหาง: ทุกกรณีเป็นค่าการเรียกแบบเรียกซ้ำหางหรือผลิตภัณฑ์กึ่งกลุ่มของทั้งสองอย่าง นอกจากนี้ตัวอย่างนี้เกิดขึ้นเพื่อใช้memptyสำหรับกรณีใดกรณีหนึ่ง แต่ถ้าเราไม่ต้องการสิ่งนั้นเราสามารถทำมันด้วยประเภททั่วไปที่กว้างSemigroupขึ้น
มาโหลดโปรแกรมนี้ใน GHCI แล้วดูว่ามันทำงานอย่างไร:
*StylizedRec> getProduct $ pow 2 4
16
*StylizedRec> getProduct $ pow 7 2
49
โปรดจำไว้ว่าวิธีการที่เราประกาศpowหาทั่วไปMonoidซึ่งชนิดที่เราเรียกว่าa? เราให้ข้อมูล GHCI พอจะอนุมานได้ว่าประเภทaที่นี่เป็นที่Product Integerซึ่งเป็นinstanceของMonoidที่มี<>การดำเนินงานที่เป็นจำนวนเต็มคูณ ดังนั้นpow 2 4ขยายซ้ำไป2<>2<>2<>2ซึ่งเป็นหรือ2*2*2*2 16จนถึงตอนนี้ดีมาก
แต่ฟังก์ชั่นของเราใช้การดำเนินการ monoid ทั่วไปเท่านั้น ก่อนหน้านี้ผมบอกว่ามีตัวอย่างของผู้อื่นMonoidเรียกว่าSumที่มีการดำเนินการคือ<> +เราลองได้ไหม
*StylizedRec> getSum $ pow 2 4
8
*StylizedRec> getSum $ pow 7 2
14
การขยายตัวเดียวกันในขณะนี้จะช่วยให้เราแทน2+2+2+2 2*2*2*2การคูณคือการบวกตามการยกกำลังคือการคูณ!
แต่ฉันให้อีกตัวอย่างหนึ่งของ Hoidell monoid: รายการที่มีการดำเนินการเรียงต่อกัน
*StylizedRec> pow [2] 4
[2,2,2,2]
*StylizedRec> pow [7] 2
[7,7]
เขียน[2]บอกคอมไพเลอร์ที่ว่านี้เป็นรายการ<>ที่อยู่ในรายการเป็น++เพื่อให้เป็น[2]++[2]++[2]++[2][2,2,2,2]
ในที่สุดอัลกอริทึม (สองในความเป็นจริง)
เพียงแค่แทนที่xด้วย[x]คุณจะแปลงอัลกอริทึมทั่วไปที่ใช้การเรียกซ้ำแบบโมดูโลแบบกลุ่มย่อยเป็นแบบที่สร้างรายการ รายการใด รายการขององค์ประกอบขั้นตอนวิธีนำไปใช้<>ในการ เนื่องจากเราใช้การดำเนินการของกลุ่มย่อยที่มีรายชื่อด้วยเช่นกันรายการผลลัพธ์จะเป็นรูปแบบของการคำนวณดั้งเดิม และเนื่องจากการดำเนินการดั้งเดิมนั้นเชื่อมโยงกันเราสามารถประเมินองค์ประกอบจากด้านหลังไปข้างหน้าหรือจากด้านหน้าไปด้านหลังได้ดีพอ ๆ กัน
หากอัลกอริทึมของคุณถึงกรณีพื้นฐานและยุติรายการจะไม่ว่างเปล่า เนื่องจากเคสเทอร์มินัลส่งคืนบางสิ่งซึ่งจะเป็นองค์ประกอบสุดท้ายของรายการดังนั้นมันจะมีองค์ประกอบอย่างน้อยหนึ่งรายการ
คุณจะใช้การดำเนินการลดเลขฐานสองกับทุกองค์ประกอบของรายการตามลำดับได้อย่างไร ถูกต้องพับ ดังนั้นคุณจึงสามารถใช้แทน[x]สำหรับการxรับรายการขององค์ประกอบที่จะลดลง<>และจากนั้นพับขวาหรือซ้ายพับรายการ:
*StylizedRec> getProduct $ foldr1 (<>) $ pow [Product 2] 4
16
*StylizedRec> import Data.List
*StylizedRec Data.List> getProduct $ foldl1' (<>) $ pow [Product 2] 4
16
รุ่นที่มีfoldr1อยู่จริงในห้องสมุดมาตรฐานเช่นsconcatสำหรับSemigroupและสำหรับmconcat Monoidมันเป็นการพับที่ด้านขวาของรายการ นั่นคือมันขยายไป[Product 2,Product 2,Product 2,Product 2]2<>(2<>(2<>(2)))
สิ่งนี้ไม่ได้มีประสิทธิภาพในกรณีนี้เพราะคุณไม่สามารถทำอะไรกับข้อกำหนดของแต่ละบุคคลจนกว่าคุณจะสร้างพวกเขาทั้งหมด (ถึงจุดหนึ่งฉันมีการสนทนาที่นี่เกี่ยวกับเมื่อใช้พับด้านขวาและเมื่อใช้เท่าซ้ายที่เข้มงวด แต่มันไปไกลเกินไป)
รุ่นที่มีfoldl1'รอยพับด้านซ้ายที่ประเมินโดยเคร่งครัด กล่าวคือเป็นฟังก์ชั่นแบบเรียกซ้ำด้วยการสะสมอย่างเข้มงวด สิ่งนี้จะประเมินให้(((2)<>2)<>2)<>2คำนวณทันทีและไม่จำเป็นเมื่อจำเป็น (อย่างน้อยมีความล่าช้าไม่มีภายในพับตัวเอง:. รายการที่ถูกพับถูกสร้างขึ้นที่นี่โดยฟังก์ชั่นอื่น ๆ ที่อาจจะมีการประเมินผลขี้เกียจ) ดังนั้นคำนวณพับ(4<>2)<>2แล้วทันทีคำนวณแล้ว8<>2 16นี่คือเหตุผลที่เราต้องการให้การดำเนินการเชื่อมโยงกัน: เราเพิ่งเปลี่ยนการจัดกลุ่มของวงเล็บ!
รอยพับด้านซ้ายที่เข้มงวดนั้นเทียบเท่ากับสิ่งที่ GCC กำลังทำอยู่ หมายเลขซ้ายสุดในตัวอย่างก่อนหน้าคือตัวสะสมในกรณีนี้คือผลิตภัณฑ์ที่กำลังทำงาน ในแต่ละขั้นตอนมันจะถูกคูณด้วยหมายเลขถัดไปในรายการ อีกวิธีในการแสดงนั่นคือ: คุณวนซ้ำค่าที่จะคูณทำให้ผลิตภัณฑ์ที่กำลังทำงานอยู่ในตัวสะสมและในแต่ละรอบซ้ำคุณคูณตัวสะสมด้วยค่าถัดไป นั่นคือมันเป็นwhileวงในการปลอมตัว
บางครั้งมันก็สามารถทำได้อย่างมีประสิทธิภาพ คอมไพเลอร์อาจสามารถปรับโครงสร้างรายการข้อมูลในหน่วยความจำให้เหมาะสม ในทางทฤษฎีมันมีข้อมูลเพียงพอที่รวบรวมเวลาที่จะคิดออกก็ควรทำได้ที่นี่: [x]เป็นเดี่ยวจึงเป็นเช่นเดียวกับ[x]<>xs cons x xsการทำซ้ำแต่ละครั้งของฟังก์ชั่นอาจจะสามารถใช้กรอบสแต็คเดียวกันอีกครั้งและอัปเดตพารามิเตอร์ให้เข้าที่
การพับแบบด้านขวาหรือแบบพับด้านซ้ายอย่างเข้มงวดอาจเหมาะสมกว่าในบางกรณีดังนั้นควรรู้ว่าคุณต้องการแบบใด นอกจากนี้ยังมีบางสิ่งที่สามารถพับครึ่งทางขวาเท่านั้น (เช่นสร้างเอาต์พุตแบบโต้ตอบโดยไม่ต้องรออินพุตทั้งหมดและทำงานในรายการที่ไม่มีที่สิ้นสุด) อย่างไรก็ตามที่นี่เรากำลังลดลำดับของการปฏิบัติงานให้เป็นค่าแบบง่ายดังนั้นรอยพับด้านซ้ายที่เข้มงวดคือสิ่งที่เราต้องการ
ดังนั้นอย่างที่คุณเห็นมันเป็นไปได้ที่จะปรับออเดอร์โมดูโลแบบเรียกซ้ำแบบหางโดยอัตโนมัติ semigroup ใด ๆ (ตัวอย่างหนึ่งซึ่งเป็นประเภทตัวเลขใด ๆ ที่ปกติภายใต้การคูณ) เป็นการพับแบบขี้เกียจขวา Haskell
สรุปเพิ่มเติม
อาร์กิวเมนต์สองตัวของการดำเนินการแบบไบนารีไม่จำเป็นต้องเป็นชนิดเดียวกันตราบใดที่ค่าเริ่มต้นเป็นประเภทเดียวกันกับผลลัพธ์ของคุณ (แน่นอนว่าคุณสามารถพลิกอาร์กิวเมนต์ได้เสมอเพื่อให้ตรงกับคำสั่งของรอยพับที่คุณทำอยู่ทางซ้ายหรือขวา) ดังนั้นคุณอาจเพิ่มแพตช์ลงในไฟล์ซ้ำ ๆ เพื่อรับไฟล์ที่อัพเดตหรือเริ่มต้นด้วยค่าเริ่มต้นที่ 1.0 หารด้วยจำนวนเต็มเพื่อสะสมผลลัพธ์ทศนิยม หรือเติมองค์ประกอบลงในรายการว่างเพื่อรับรายการ
การวางนัยทั่วไปอีกประเภทหนึ่งคือการนำ fold ไปใช้กับรายการ แต่ใช้กับFoldableโครงสร้างข้อมูลอื่น บ่อยครั้งที่รายการลิงก์เชิงเส้นที่ไม่เปลี่ยนรูปไม่ได้เป็นโครงสร้างข้อมูลที่คุณต้องการสำหรับอัลกอริทึมที่กำหนด ปัญหาหนึ่งที่ฉันไม่ได้กล่าวถึงข้างต้นก็คือมันมีประสิทธิภาพมากขึ้นในการเพิ่มองค์ประกอบไว้ที่ด้านหน้าของรายการมากกว่าไปทางด้านหลังและเมื่อการดำเนินการไม่เปลี่ยนไปใช้xทางด้านซ้ายและด้านขวาของการดำเนินการไม่ได้ เหมือน. ดังนั้นคุณจะต้องใช้โครงสร้างอื่นเช่นคู่ของรายการหรือต้นไม้ไบนารีเพื่อแสดงอัลกอริทึมที่สามารถใช้xทางด้านขวาของ<>เช่นเดียวกับทางด้านซ้าย
นอกจากนี้โปรดทราบว่าคุณสมบัติการเชื่อมโยงช่วยให้คุณสามารถจัดกลุ่มการดำเนินการในรูปแบบที่เป็นประโยชน์อื่น ๆ เช่นการแบ่งและพิชิต:
times :: Monoid a => a -> Word -> a
times _ 0 = mempty
times x 1 = x
times x n | even n = y <> y
| otherwise = x <> y <> y
where y = times x (n `quot` 2)
หรือการขนานแบบอัตโนมัติโดยที่แต่ละเธรดจะลดช่วงย่อยให้เป็นค่าที่รวมกับส่วนอื่น ๆ
if(n==0) return 0;(ไม่คืนค่า 1 เช่นเดียวกับในคำถามของคุณ)x^0 = 1นั่นเป็นข้อผิดพลาด ไม่ใช่ว่ามันสำคัญสำหรับส่วนที่เหลือของคำถาม แต่; asm ซ้ำจะตรวจสอบกรณีพิเศษก่อน แต่ที่แปลกการใช้งานซ้ำแนะนำจำนวน1 * xที่ไม่ได้อยู่ในแหล่งที่มาแม้ว่าเราจะทำfloatรุ่น gcc.godbolt.org/z/eqwine (และ gcc ทำได้สำเร็จเท่านั้น-ffast-math)