AT_abc413_e题解

AtCoder413 E - Reverse 2^i 题解

题目传送门

vjudge(+中文翻译) Reverse 2^i - AtCoder abc413_e
AtCoder413 E - Reverse 2^i

题面

这道题给了我们一个数组 \(P\) ,长度为 \(2^N\) ,题目保证 \(P\)\((1,2,3,...,2^N)\) 的一个排列。
我们可以做以下操作任意次:

  • 选择一个整数 \(b\) \((0 \leq b \leq N)\),将 \(P\) 从左往右每 \(2^b\) 个数字分成一段,然后任意选择一段进行左右翻转。

输出能获得的所有新的 \(P\) 数组中字典序最小的那个。

思路

思路1

思路1-思路

首先观察到:对于任意一个区间,都可以将最小值放到最前面。但是后面的次小值可能无法放到第二位,因为移动的时候会带动其他的数字,次小值如果放到第二位,可能会影响最小值的位置。

所以我们要避免出现影响。

明显:只要翻转的区间不盖到这个位置即可。

如果看最大的区间,那么我们的思路就是:先将最小值放到这个区间最前面,然后去考虑那些不会影响到最小值位置的区间(注意:我们考虑的区间长度必须是 \(2^k\) 且将原数组从左往右每 \(2^k\) 个数字分成一段,该区间正好占一段)

而对于小区间,我们发现每次讨论的问题是相同的,递归即可。

思路1-方法:

对于每个区间
1、找到这个区间中最小值的下标
2、我们有一个右指针,最开始右指针指着区间右端,如果这个最小值的下标更靠近右指针,那么将整个区间左端到右指针的区间翻转,使得最小值的下标靠近区间左端,然后将右指针改为原本左指针和右指针的中间。
3、重复2直到右指针和左指针都在区间左端,那么此时最小值也在区间左端。
4、重置右指针为区间右端,每次找到区间左端和右指针的中间,记为 \(t\) ,递归处理 \(t+1\) 到 右指针 的区间,然后将右指针改为t
5、重复4直到右指针与区间左端的中间为区间左端。

思路1-代码:

#include <iostream>
#include <algorithm>
using namespace std;
int n;
int p[(1 << 18) + 1];
void dfs(int l, int r)
{
    int t = 1;
    // 第1步,找到区间最小值的下标
    int mn = 1e9, mnid = 0;
    for (int i = l; i <= r; i++)
    {
        if (p[i] < mn)
        {
            mn = p[i];
            mnid = i;
        }
    }
    // 第2到3步,t表示左端和右指针的中间。
    t = (r + l) / 2;
    int ls = r;
    while (ls != l)
    {
        if (mnid > t) // 最小值在中点右边
        {
            reverse(p + l, p + ls + 1);
            mnid = l + (ls - mnid); // 注意最小值位置也要跟着翻转
        }
        ls = t;
        t = (l + t) / 2;
    }
    // 第4到5步,继续考虑较小的区间
    t = (r + l) / 2;
    ls = r;
    while (t != l)
    {
        dfs(t + 1, ls);
        ls = t;
        t = (l + t) / 2;
    }

}
void solve()
{
    cin >> n;
    for (int i = 1; i <= (1 << n); i++)
    {
        cin >> p[i];
    }
    dfs(1, (1 << n));
    for (int i = 1; i <= (1 << n); i++)
    {
        cout << p[i] << ' ';
    }
    cout << endl;
    return;
}
int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        solve();
    }
    return 0;
}

思路2

思路2-思路

对于任何区间,翻转两次等于不翻,所以最多翻一次。

并且显然只有区间最小值在右边的时候翻一次。

对于每个区间的最小值,他移到左半边后,一定是这个区间的左半边的最小值,所以它最终会移到这个区间所属位置的最左边。
而对于右半边,也会找出最小值并放到右半边所属位置的最左边,并且右半边翻转影响不到左半边,左半边翻转影响不到右半边。
每一个区间都会这样,直到最后,就可以找到最小的字典序了。

所以对于某个区间我们只需要找到最小值的位置,如果最小值在这个区间的右半边,则翻转一次,否则不翻转,然后使用递归继续分开考虑左半边和右半边。

思路2-方法:

对于每个区间
1、找到这个区间中最小值的下标
2、找出这个区间的中心点。
3、如果这个最小值更靠近右端,就翻转这个区间,这样最小值就在左半边了。
4、递归处理左端点到中心点的区间和中心点+1到右端点的区间。

思路2-代码

#include <iostream>
#include <algorithm>
using namespace std;
int n;
int p[(1 << 18) + 1];
void dfs(int l, int r)
{
    // 退出判断
    if (l == r) return;
    // 第1步,找到最小值下标
    int mn = 1e9, mnid = 0;
    for (int i = l; i <= r; i++)
    {
        if (p[i] < mn)
        {
            mn = p[i];
            mnid = i;
        }
    }
    int t = (l + r) / 2; // 第2步,找中点
    if (mnid > t) // 第3步,如果最小值靠近右端,则翻转区间。
    {
        reverse(p + l, p + r + 1);
    }
    dfs(l, t); // 考虑左半边
    dfs(t + 1, r); // 考虑右半边
}
void solve()
{
    cin >> n;
    for (int i = 1; i <= (1 << n); i++)
    {
        cin >> p[i];
    }
    dfs(1, (1 << n));
    for (int i = 1; i <= (1 << n); i++)
    {
        cout << p[i] << ' ';
    }
    cout << endl;
    return;
}
int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        solve();
    }
    return 0;
}

posted @ 2025-07-08 11:44  MichaelZeng  阅读(12)  评论(0)    收藏  举报