博弈?题解

写在前面

上次写博客已经是两年前的事情了,明天就网络赛了,心中烦躁,一时也不知道写什么好,故此传一下这道题的题解,也算是我出的第一道题,题目不难,还是挺有纪念意义的。

题目回顾

给定长度为 $n$ 的排列 $p[1..n]$。Alice 和 Bob 轮流操作,Alice 先手。每一步,玩家必须选择一个位置 $i $且满足 $ p[i]≠i$,然后执行一次交换操作:$swap(p[i], p[p[i]]);$

如果当前排列已经是恒等排列,即对所有 $j$ 都有 $p[j]=j$,则无法再走,当前玩家失败。问:在双方都最优的情况下,Alice(先手)是否必赢?

并没有所谓的最优策略

如果你实际操作一下,你会发现,每一次合法的操作,都会贡献一个$p[i] = i$,而且这个是不可变的,也就是说,根本不存在所谓的最优策略,每一个合法的策略都是最优策略,所以这或许都称不上一个博弈的题目。


“环”的被发现

我们再尝试模拟一下,对于一个这样的数据

2 3 1 6 4 5
1 2 3 4 5 6

我们手摸一下这个数据,可以发现,这个数据的操作被完美的分成了两部分,1-3是一部分,4-6是一部分。并且当我们把p[i]作为指向来思考,就可以发现,这是一个不折不扣的“环”,第一个位置指向第二个位置,第二个位置指向第三个位置,第三个位置反过来又指向第一个位置。这有什么意义呢?这意味着,如果我们将某个位置作为中间商,来不断交换,只需要k-1次交换( k是这个“环”中元素的个数),这个“环”就能被还原。如果熟悉魔方盲拧的同学应该知道我说的是什么,我们继续用这个例子来讲解:

对于前面的那个“环”,我们让1号位做中间商。那么1->2,1号能和2号交换,变成了:

3 2 1
1 2 3

现在1上面是3,也就是1->3,1号和3号交换,这个“环”就还原了。这个首尾相接的“环”实际上也就是还原的次序。


次数的奇偶与结果

对于每一个”环“,都是$k-1$次交换,那么不难发现,总共需要的交换次数:(假设一共有$c$个环) \(\sum_{j=1}^{c} (k_{j} -1) = (\sum_{j=1}^{c}k_j) - c = n - c\) 也就是说,我只需看$n-c$次行动,是$Alice$赢还是$Bob$赢,显而易见,奇数次是$Alice$,偶数次是$Bob$。

其实如果说你不想推式子,也没有发现“环”,但是你知道每次交换都会产生一个$p[i] = i$,直接模拟也是可以的,对于每一个$ p[i]≠i$ 的地方,反复的执行$swap$,因为每次肯定会产生一个答案,复杂度也是$O(n)$的。


复杂度分析

  • 时间复杂度:
    • 判断整个有多少个环,只需要所有遍历一遍,整体 $O(n)$。

完整 C++ 实现

#include <bits/stdc++.h>
using namespace std;

// 用于比较常见的 输出"YES"和"NO"题目
#define returnNo return void(puts("No"))
#define returnYes return void(puts("Yes"))
//

// 用于 debug函数,只会在本地编辑器才显示
template <typename A, typename B>
ostream &operator<<(ostream &os, const pair<A, B> &p) { return os << '(' << p.first << ", " << p.second << ')'; }
template <typename T_container, typename T = typename enable_if<!is_same<T_container, string>::value, typename T_container::value_type>::type>
ostream &operator<<(ostream &os, const T_container &v)
{
    os << '[';
    string sep;
    for (const T &x : v)
        os << sep << x, sep = ", ";
    return os << ']';
}

void debug_out() { cerr << endl; }
template <typename Head, typename... Tail>
void debug_out(Head H, Tail... T)
{
    cerr << ' ' << H;
    debug_out(T...);
}
#ifdef LOCAL
#define debug(...) cerr << "[" << #__VA_ARGS__ << "]:", debug_out(__VA_ARGS__)
#else
#define debug(...)
#endif
//

// 常用头
#define int long long
#define endl '\n'
constexpr int N = 2e5 + 10;

void solve()
{
    int n;
    cin >> n;
    vector<int> v(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i];
    }
    vector<int> vis(n + 1, false);
    int c = 0;

    for (int i = 1; i <= n; i++)
    {
        if (!vis[i])
        {
            c++;
            int u = i;
            while (!vis[u])
            {
                vis[u] = true;
                u = v[u];
            }
        }
    }
    if ((n - c) % 2 == 1)
        cout << "YES" << '\n';
    else
        cout << "NO" << '\n';
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _ = 1;
    while (_--)
        solve();
}

奇排列?偶排列?

如果你学习过线性代数,那么对奇排列和偶排列一定不陌生。在这道题里,我们已经论证过了,并没有所谓的最优策略,胜负完全是由次数来决定的,而我们在线性代数中学过,只要交换两个数,那么奇排列就会变成偶排列,偶排列就会变成奇排列。对于恒等排列而言,肯定是偶排列。也就是说,我们要经过若干次交换,让这个排列变成偶排列。也就是说,如果说原来排列是奇排列,那么肯定是经过奇数此次交换,那么Alice赢,如果是偶排列,则是偶数次交换,那么是$Bob$赢。

因此我们可以通过判断这个排列是奇排列还是,偶排列来确定这道题的答案。而确定奇排列和偶排列,我们可以通过归并排序的方法,也可以使用树状数组的方法来求解这个题

下面是树状数组的示例代码:

#include <bits/stdc++.h>
using namespace std;

// 用于比较常见的 输出"YES"和"NO"题目
#define returnNo return void(puts("No"))
#define returnYes return void(puts("Yes"))
//

// 用于 debug函数,只会在本地编辑器才显示
template <typename A, typename B>
ostream &operator<<(ostream &os, const pair<A, B> &p) { return os << '(' << p.first << ", " << p.second << ')'; }
template <typename T_container, typename T = typename enable_if<!is_same<T_container, string>::value, typename T_container::value_type>::type>
ostream &operator<<(ostream &os, const T_container &v)
{
    os << '[';
    string sep;
    for (const T &x : v)
        os << sep << x, sep = ", ";
    return os << ']';
}

void debug_out() { cerr << endl; }
template <typename Head, typename... Tail>
void debug_out(Head H, Tail... T)
{
    cerr << ' ' << H;
    debug_out(T...);
}
#ifdef LOCAL
#define debug(...) cerr << "[" << #__VA_ARGS__ << "]:", debug_out(__VA_ARGS__)
#else
#define debug(...)
#endif
//

// 常用头
#define int long long
#define endl '\n'
constexpr int N = 2e5 + 10;

int n, ans;
int s[N];
vector<int> arr;

int lowbit(int x)
{
    return x & (-x);
}

void update(int x, int k)
{
    for (; x <= N; x += lowbit(x))
        s[x] += k;
}

int getsum(int x)
{
    int sum = 0;
    for (; x > 0; x -= lowbit(x))
        sum += s[x];
    return sum;
}

int query(int l, int r)
{
    return getsum(r) - getsum(l - 1);
}

void solve()
{
    cin >> n;
    for (int i = 1, t; i <= n; i++)
    {
        cin >> t;
        arr.push_back(t);
    }
    vector<int> tmp(arr);
    sort(tmp.begin(), tmp.end());
    for (int i = 0; i < n; i++)
    {
        arr[i] = lower_bound(tmp.begin(), tmp.end(), arr[i]) - tmp.begin() + 1;
        update(arr[i], 1);
        ans += query(arr[i] + 1, n + 1);
    }
    if (ans % 2 == 1)
        cout << "YES" << '\n';
    else
        cout << "NO" << '\n';
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _ = 1;
    while (_--)
        solve();
}