C++ #198-ի ամենօրյա բիթ(ե), Ընդհանուր հարցազրույցի խնդիր. Ամենաերկար պալինդրոմային ենթատող:

Այսօր մենք կանդրադառնանք հարցազրույցի ընդհանուր խնդրին. Ամենաերկար palindromic substring.

Ստանալով տողը որպես std::string_view, վերադարձրեք ամենաերկար palindromic ենթատողը (եթե բազմապատիկ, ապա առաջինը):

Վերոնշյալ օրինակում ամենամեծ պալինդրոմային ենթաշարը «բաբ» է:

Նախքան լուծումը կարդալը շարունակելը, ես խրախուսում եմ ձեզ փորձել ինքներդ լուծել այն: Ահա Compiler Explorer-ի հղումը մի քանի փորձարկման դեպքերով. https://compiler-explorer.com/z/fKT5MzoGE:

Լուծում

Կտրուկ ուժի լուծումը (ստուգելով յուրաքանչյուր ենթալարի պալինդրոմի համար) կվերցնի O(n³): Մենք կարող ենք ավելի լավ անել:

Առաջին բարելավումը պալինդրոմների բնորոշ համաչափությունից օգտվելն է: Պալինդրոմներն ընդարձակվում են իրենց կենտրոնից, որը մենք կարող ենք ընդօրինակել մեր որոնման մեջ: Մենք ընդլայնվում ենք յուրաքանչյուր պոտենցիալ կենտրոնից մինչև չհանդիպենք երկու նիշերի, որոնք չեն համընկնում: Հիշեք, որ կենտրոնը կարող է լինել կերպար կամ երկու նիշերի միջև եղած բացը: Մենք կարող ենք ի վերջո կատարել O(n) համեմատություններ յուրաքանչյուր կենտրոնի համար, ինչը հանգեցնում է O(n²) ընդհանուր բարդության:

std::string_view longest_palindrome(std::string_view s) {
    int64_t left = 0;
    int64_t len = 0;

    // For every centre:
    for (int64_t c = 0; c < std::ssize(s); ++c) {
        // The centre is a character:
        for (int64_t i = c, j = c; 
             i >= 0 && j < std::ssize(s) && s[i] == s[j];
             --i,++j) {
            if (j-i+1 > len) { // Update max
                left = i;
                len = j-i+1;
            }
        }
        // The centre is in between characters:
        for (int64_t i = c, j = c+1;
             i >= 0 && j < std::ssize(s) && s[i] == s[j];
             --i,++j) {
            if (j-i+1 > len) { // Update max
                left = i;
                len = j-i+1;
            }
        }
    }
    return s.substr(left, len);
}

Բացեք լուծումը Compiler Explorer-ում:

Թեև վերը նշված մոտեցումը միանգամայն վավեր է, մենք կարող ենք փրկել որոշ խնդիրներ և մի քանի նիշ՝ կիրառելով տեսակետներ և ալգորիթմներ խնդրին:

#include <ranges>

std::string_view longest_palindrome_2(std::string_view s) {
    std::string_view result;

    for (int64_t c = 0; c < std::ssize(s); ++c) {
        auto left = s | std::views::take(c) | std::views::reverse;

        // Even sized palindromes
        auto right = s | std::views::drop(c);
        auto [le,re] = std::ranges::mismatch(left,right);
        if (re-le.base() > std::ssize(result))
            result = std::string_view(le.base(), re);
        // Odd sized palindromes
        auto right_odd = s | std::views::drop(c+1);
        auto [lo,ro] = std::ranges::mismatch(left,right_odd);
        if (ro-lo.base() > std::ssize(result))
            result = std::string_view(lo.base(), ro);
    }
    return result;
}

Բացեք լուծումը Compiler Explorer-ում:

Վերոնշյալ մոտեցումներից որևէ մեկը պետք է բավարար լինի հարցազրույցի համար: Այնուամենայնիվ, կա O(n) ալգորիթմ: Եկեք մտածենք մեր մոտեցման անարդյունավետության մասին:

Ենթադրենք, մենք հենց նոր պարզեցինք, որ կենտրոնում գտնվող պալինդրոմը, որը նշանակում է սպիտակ բջիջ, ունի չորս շառավիղ (այսինքն՝ ընդհանուր երկարությունը 9), և երկու կարմիր նիշերը տարբեր են: Սովորաբար մենք կանցնեինք հաջորդ կենտրոն, բայց մենք ունենք շատ տեղեկատվություն, որը չենք օգտագործում: Քանի որ պալինդրոմը սիմետրիկ է, մենք կարող ենք նորից օգտագործել այն տեղեկատվությունը, որը մենք հաշվարկել ենք նախորդ կենտրոնների համար:

Եթե ​​նախորդ կենտրոնում ամենամեծ պալինդրոմը ամբողջությամբ պարունակվում է մեր նոր պալինդրոմում, մենք կարող ենք պատճենել այս տեղեկատվությունը, քանի որ նախորդ պալինդրոմը հայելային բովանդակության մի մասն է:

Եթե ​​նախորդ կենտրոնում գտնվող ամենամեծ պալինդրոմը դուրս է գալիս ձախ կողմում գտնվող մեր ներկայիս պալինդրոմից, մենք ունենք մի փոքր ավելի բարդ իրավիճակ: Մենք գիտենք, որ դրա մի մասն արդեն հայելային է։ Այս դեպքում դա երեք շառավղով պալինդրոմ է։ Կարո՞ղ է այս պալինդրոմը դեռ աճել: Բարեբախտաբար, դա չի կարող: Եթե ​​այն հասնի չորս չափի, դա կստիպի երկու կարմիր նիշերը նույնը լինել. սակայն, դա հակասում է մեր նախապայմաններին։

Վերջապես, եկեք դիտարկենք մի պալինդրոմ, որը ճշգրտորեն տեղավորվում է սահմանի մեջ: Սա ներկայացնում է միակ դեպքը, երբ նոր պալինդրոմը դեռ կարող է աճել, քանի որ մենք չենք կարող բացառել, որ նարնջագույն բջիջը համապատասխանում է ճիշտ կարմիր բջիջին: Այնուամենայնիվ, մենք չպետք է սկսենք զրոյից. Մենք արդեն գիտենք, որ շառավիղը առնվազն երեք է։

Դնելով վերը նշված դիտարկումները՝ մենք ի վերջո հայտնվում ենք Manacher-ի ալգորիթմի հետ, որը կարող է գտնել O(n-ում) ամենամեծ պալինդրոմային ենթատողը: Հետևյալը պարզեցված տարբերակ է, որը վերադարձնում է միայն առավելագույն պալինդրոմի չափը:

int64_t manachers(std::string_view s) {
    std::vector<int> lengths(s.length(), 0);
    int64_t radius = 0;
    int64_t c = 0;
    while (c < std::ssize(s)) {
        // Expand from the current centre until
        // we find non-matching characters
        while (c - (radius+1) >= 0 && 
               c + (radius+1) < std::ssize(s) && 
               s[c - (radius+1)] == s[c + (radius+1)])
            ++radius;
        lengths[c] = radius; // Record the radius

        int64_t curr_c = c;
        int64_t curr_r = radius;

        // Precalculate minimum radius for the next centre(s)
        ++c;
        radius = 0;
        while (c <= curr_c + curr_r) {
            int64_t mirror = curr_c - (c - curr_c);
            int64_t max_radius = curr_c + curr_r - c;

            // Completely mirrored palindrome
            if (lengths[mirror] < max_radius) {
                lengths[c] = lengths[mirror]; // Reuse
                ++c;
            // Palindrome that extends beyond current palindrome
            } else if (lengths[mirror] > max_radius) {
                lengths[c] = max_radius; // Truncate
                ++c;
            // Palindrome that fits exactly into the boundary
            } else {
                // Can expand but we know that max_radius
                // is already mirrored.
                // i.e. no point in rechecking in the while loop above
                radius = max_radius;
                break;
            }
        }
    }
    return 2*std::ranges::max(lengths)+1;
}

Բացեք լուծումը Compiler Explorer-ում:

Այնուամենայնիվ, դուք կարող եք նկատել, որ մենք ենթադրում ենք, որ մենք ունենք պալինդրոմի կենտրոն: Սա այս ալգորիթմի հիմնական բացասական կողմն է, որը կարելի է մեղմել՝ յուրաքանչյուր զույգ նիշերի միջև կեղծ նիշեր ներմուծելով: Այն, ինչ նախկինում կլիներ հավասարաչափ պալինդրոմ, այժմ կունենա նոր կենտրոն՝ կեղծ կերպարով: