std :: next_permutation คำอธิบายการนำไปใช้งาน


110

ฉันอยากรู้ว่าstd:next_permutationมีการใช้งานอย่างไรดังนั้นฉันจึงแยกgnu libstdc++ 4.7เวอร์ชันและทำความสะอาดตัวระบุและการจัดรูปแบบเพื่อสร้างการสาธิตต่อไปนี้ ...

#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

template<typename It>
bool next_permutation(It begin, It end)
{
        if (begin == end)
                return false;

        It i = begin;
        ++i;
        if (i == end)
                return false;

        i = end;
        --i;

        while (true)
        {
                It j = i;
                --i;

                if (*i < *j)
                {
                        It k = end;

                        while (!(*i < *--k))
                                /* pass */;

                        iter_swap(i, k);
                        reverse(j, end);
                        return true;
                }

                if (i == begin)
                {
                        reverse(begin, end);
                        return false;
                }
        }
}

int main()
{
        vector<int> v = { 1, 2, 3, 4 };

        do
        {
                for (int i = 0; i < 4; i++)
                {
                        cout << v[i] << " ";
                }
                cout << endl;
        }
        while (::next_permutation(v.begin(), v.end()));
}

ผลลัพธ์เป็นไปตามที่คาดไว้: http://ideone.com/4nZdx

คำถามของฉันคือมันทำงานอย่างไร? คือความหมายของสิ่งที่i, jและk? พวกเขามีคุณค่าอะไรในส่วนต่างๆของการดำเนินการ? ภาพร่างของหลักฐานความถูกต้องคืออะไร?

อย่างชัดเจนก่อนที่จะเข้าสู่ลูปหลักเพียงแค่ตรวจสอบกรณีรายการองค์ประกอบเล็กน้อย 0 หรือ 1 ที่รายการของลูปหลัก i กำลังชี้ไปที่องค์ประกอบสุดท้าย (ไม่ใช่หนึ่งปลายที่ผ่านมา) และรายการมีความยาวอย่างน้อย 2 องค์ประกอบ

เกิดอะไรขึ้นในเนื้อหาของลูปหลัก?


เฮ้คุณแตกรหัสชิ้นนั้นได้อย่างไร เมื่อฉันตรวจสอบ #include <อัลกอริทึม> โค้ดนั้นแตกต่างอย่างสิ้นเชิงซึ่งประกอบด้วยฟังก์ชันอื่น ๆ
Manjunath

คำตอบ:


172

ลองดูการเรียงสับเปลี่ยนบางส่วน:

1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
...

เราจะเปลี่ยนจากการเปลี่ยนแปลงหนึ่งไปยังอีกครั้งได้อย่างไร? ประการแรกเรามาดูสิ่งต่าง ๆ กันเล็กน้อย เราสามารถดูองค์ประกอบเป็นตัวเลขและพีชคณิตเป็นตัวเลข กำลังดูปัญหาที่เกิดขึ้นในลักษณะนี้เราต้องการที่จะสั่งซื้อพีชคณิต / ตัวเลขใน "จากน้อยไปมาก" สั่ง

เมื่อเราสั่งซื้อตัวเลขเราต้องการ "เพิ่มจำนวนให้น้อยที่สุด" ตัวอย่างเช่นเมื่อนับเราจะไม่นับ 1, 2, 3, 10, ... เพราะยังมี 4, 5, ... อยู่ระหว่างและถึงแม้ว่า 10 จะมากกว่า 3 แต่ก็มีตัวเลขที่ขาดหายไปซึ่งสามารถรับได้ เพิ่มขึ้น 3 โดยจำนวนที่น้อยลง ในตัวอย่างด้านบนเราจะเห็นว่า1ยังคงเป็นตัวเลขแรกเป็นเวลานานเนื่องจากมีการเรียงลำดับ "หลัก" 3 ตัวสุดท้ายจำนวนมากซึ่ง "เพิ่ม" การเรียงสับเปลี่ยนด้วยจำนวนที่น้อยลง

แล้วสุดท้ายเราจะ "ใช้" เมื่อ1ไหร่? เมื่อไม่มีการเรียงสับเปลี่ยนของ 3 หลักสุดท้ายอีกต่อไป
และเมื่อใดที่ไม่มีการเรียงสับเปลี่ยนของ 3 หลักสุดท้าย? เมื่อ 3 หลักสุดท้ายเรียงลำดับจากมากไปหาน้อย

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

กลับไปที่รหัส:

while (true)
{
    It j = i;
    --i;

    if (*i < *j)
    { // ...
    }

    if (i == begin)
    { // ...
    }
}

จาก 2 บรรทัดแรกในลูปjเป็นองค์ประกอบและiเป็นองค์ประกอบก่อนหน้านั้น
จากนั้นหากองค์ประกอบเรียงลำดับจากน้อยไปมาก ( if (*i < *j)) ให้ทำบางสิ่ง
มิฉะนั้นหากสิ่งทั้งหมดอยู่ในลำดับจากมากไปหาน้อย ( if (i == begin)) นี่คือการเปลี่ยนแปลงสุดท้าย
มิฉะนั้นเราจะดำเนินการต่อและเราเห็นว่า j และ i ลดลงเป็นหลัก

ตอนนี้เราเข้าใจif (i == begin)ส่วนนั้นแล้วดังนั้นสิ่งที่เราต้องเข้าใจก็คือif (*i < *j)ส่วนนั้น

โปรดทราบว่า: "ถ้าองค์ประกอบอยู่ในลำดับจากน้อยไปมาก ... " ซึ่งสนับสนุนข้อสังเกตก่อนหน้านี้ว่าเราต้องทำอะไรบางอย่างกับตัวเลขเท่านั้น "เมื่อทุกอย่างทางด้านขวาเรียงลำดับจากมากไปหาน้อย" คำสั่งจากน้อยไปมากifคือการค้นหาตำแหน่งทางซ้ายสุดโดยที่ "ทุกอย่างทางขวาเรียงลำดับจากมากไปหาน้อย"

ลองดูตัวอย่างอีกครั้ง:

...
1 4 3 2
2 1 3 4
...
2 4 3 1
3 1 2 4
...

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

ลองดูรหัส:

It k = end;

while (!(*i < *--k))
    /* pass */;

iter_swap(i, k);
reverse(j, end);
return true;

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

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


12
คำอธิบายที่น่าทึ่ง

2
ขอบคุณสำหรับคำอธิบาย! ขั้นตอนวิธีการนี้เรียกว่ารุ่นเพื่อทำพจนานุกรม อัลกอริทึมดังกล่าวมีจำนวนมากCombinatoricsแต่นี่เป็นวิธีที่คลาสสิกที่สุด
chain ro

1
ความซับซ้อนของอัลกอริทึมดังกล่าวคืออะไร?
user72708

leetcode มีคำอธิบายดีๆ leetcode.com/pro issues/next
permutation/solution

40

การใช้งาน gcc สร้างการเรียงสับเปลี่ยนตามลำดับศัพท์ Wikipediaอธิบายไว้ดังนี้:

อัลกอริทึมต่อไปนี้จะสร้างการเรียงสับเปลี่ยนลำดับถัดไปตามศัพท์หลังจากการเปลี่ยนแปลงที่กำหนด มันเปลี่ยนการเปลี่ยนแปลงที่กำหนดในสถานที่

  1. ค้นหาดัชนีที่ใหญ่ที่สุด k เช่น [k] <a [k + 1] หากไม่มีดัชนีดังกล่าวการเรียงสับเปลี่ยนจะเป็นการเปลี่ยนรูปครั้งสุดท้าย
  2. ค้นหาดัชนีที่ใหญ่ที่สุด l เช่น [k] <a [l] เนื่องจาก k + 1 เป็นดัชนีดังกล่าว l จึงถูกกำหนดไว้อย่างดีและเป็นไปตาม k <l
  3. สลับ [k] กับ [l]
  4. ย้อนกลับลำดับจาก [k + 1] ถึงและรวมองค์ประกอบสุดท้าย a [n]

AFAICT การใช้งานทั้งหมดจะสร้างลำดับเดียวกัน
MSalters

12

นูไปลงในเชิงลึกเกี่ยวกับขั้นตอนวิธีการนี้และภาพรวมในส่วน 7.2.1.2 และ 7.2.1.3 ของศิลปะของการเขียนโปรแกรมคอมพิวเตอร์ เขาเรียกมันว่า "Algorithm L" - เห็นได้ชัดว่ามันมีอายุย้อนกลับไปในศตวรรษที่ 13


1
กรุณาเอ่ยชื่อหนังสือได้ไหม
Grobber

3
TAOCP = The Art of Computer Programming

9

นี่คือการใช้งานที่สมบูรณ์โดยใช้อัลกอริทึมไลบรารีมาตรฐานอื่น ๆ :

template <typename I, typename C>
    // requires BidirectionalIterator<I> && Compare<C>
bool my_next_permutation(I begin, I end, C comp) {
    auto rbegin = std::make_reverse_iterator(end);
    auto rend = std::make_reverse_iterator(begin);
    auto rsorted_end = std::is_sorted_until(rbegin, rend, comp);
    bool has_more_permutations = rsorted_end != rend;
    if (has_more_permutations) {
        auto next_permutation_rend = std::upper_bound(
            rbegin, rsorted_end, *rsorted_end, comp);
        std::iter_swap(rsorted_end, next_permutation_rend);
    }
    std::reverse(rbegin, rsorted_end);
    return has_more_permutations;
}

การสาธิต


1
สิ่งนี้เน้นย้ำถึงความสำคัญของชื่อตัวแปรที่ดีและการแยกข้อกังวล เป็นข้อมูลที่มากกว่าis_final_permutation begin == end - 1การเรียกis_sorted_until/ upper_boundแยกตรรกะการเรียงสับเปลี่ยนออกจากการดำเนินการเหล่านั้นและทำให้เข้าใจได้มากขึ้น นอกจากนี้ upper_bound ยังเป็นการค้นหาแบบไบนารีในขณะที่while (!(*i < *--k));เป็นแบบเชิงเส้นดังนั้นจึงมีประสิทธิภาพมากกว่า
Jonathan Gawrych

1

มีอธิบายตนเอง implemetation เป็นไปได้ในcppreference<algorithm>ใช้

template <class Iterator>
bool next_permutation(Iterator first, Iterator last) {
    if (first == last) return false;
    Iterator i = last;
    if (first == --i) return false;
    while (1) {
        Iterator i1 = i, i2;
        if (*--i < *i1) {
            i2 = last;
            while (!(*i < *--i2));
            std::iter_swap(i, i2);
            std::reverse(i1, last);
            return true;
        }
        if (i == first) {
            std::reverse(first, last);
            return false;
        }
    }
}

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

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