算法杂记 2023/02/26

好久没有自己写出过一题了,尴尬。这题WA了足足 6 发才写出来。这种偏模拟一些的我还是太薄弱了。

Game on Axis 1900

题面翻译

题目描述

\(n\) 个点,第 \(i\) 个点上有数字 \(a_i\),你在这些点上面玩一个游戏,初始在点 \(1\)。当你在点 \(i\) 时:

  • \(1\le i\le n\),则你会跳到点 \(i+a_i\)
  • 否则游戏结束。

在游戏开始前,你可以选择一对 \(x\)\(y\)\(1\le x\le n,-n\le y\le n\)),并将 \(a_x\) 赋值为 \(y\)。请求出这样的 \((x,y)\) 的对数使得可以在有限步内结束游戏。注意即使 \(a_x\) 已经等于 \(y\) 了,\((x,y)\) 也可能是合法的。

输入格式

输入第一行一个正整数 \(t\)\(1\le t\le 10^4\))表示数据组数。

每组测试数据第一行一个正整数 \(n\)\(1\le n\le 2\cdot 10^5\))表示点的个数。

第二行 \(n\) 个正整数 \(a_1,a_2,\ldots,a_n\)\(-n \le a_i \le n\))表示序列 \(a\)

保证所有测试数据的 \(n\) 之和不超过 \(2\cdot 10^5\)

样例解释

第一组数据中,符合条件的 \((x,y)\)\((1,-1)\)\((1,1)\),分别对应路径 \(1\rightarrow 0\)\(1\rightarrow 2\)。注意 \((1,2)\) 不合法因为 \(n=1\)\(y=2\) 不符合 \(-n\le y\le n\)\((1,0)\) 也不合法因为你将永远从 \(1\) 走到 \(1\) 而无法结束游戏。

第二组数据中,符合条件的 \((x,y)\)\((1,-2),(1,-1),(1,2),(2,-2),(2,-1),(2,0),(2,1),(2,2)\)

第三组数据中,符合条件的 \((x,y)\)\((1,-2),(1,-1),(1,1),(1,2),(2,-2),(2,1),(2,2)\)

题目描述

There are $ n $ points $ 1,2,\ldots,n $ , each point $ i $ has a number $ a_i $ on it. You're playing a game on them. Initially, you are at point $ 1 $ . When you are at point $ i $ , take following steps:

  • If $ 1\le i\le n $ , go to $ i+a_i $ ,
  • Otherwise, the game ends.

Before the game begins, you can choose two integers $ x $ and $ y $ satisfying $ 1\le x\le n $ , $ -n \le y \le n $ and replace $ a_x $ with $ y $ (set $ a_x := y $ ). Find the number of distinct pairs $ (x,y) $ such that the game that you start after making the change ends in a finite number of steps.

Notice that you do not have to satisfy $ a_x\not=y $ .

输入格式

Each test contains multiple test cases. The first line contains an integer $ t $ ( $ 1\le t\le 10^4) $ — the number of test cases.

The first line of each test case contains one integer $ n $ ( $ 1\le n\le 2\cdot 10^5 $ ) — the number of points.

The second line contains $ n $ integers $ a_1,a_2,\ldots,a_n $ ( $ -n \le a_i \le n $ ) — the numbers on the axis.

It's guaranteed that the sum of $ n $ does not exceed $ 2\cdot 10^5 $ .

输出格式

For each test case, print a line containing a single integer — the number of distinct pairs $ (x,y) $ with which the game ends.

样例 #1

样例输入 #1

9
1
0
2
-1 0
2
1 -1
2
1 1
3
-1 -2 -1
3
1 -2 -1
4
-1 4 -2 1
5
1 1 1 1 -4
5
1 1 1 1 1

样例输出 #1

2
8
6
7
20
17
34
30
40

提示

In the first test case, the pairs $ (x,y) $ with which the game ends are $ (1,-1) $ and $ (1,1) $ , corresponding to the routes $ 1\rightarrow 0 $ and $ 1\rightarrow 2 $ . Note that $ (1,2) $ is invalid since when $ n=1 $ , $ y=2 $ violates $ -n\le y\le n $ . $ (1,0) $ is also invalid since you will go from $ 1 $ to $ 1 $ forever.

In the second test case, the pairs are $ (1,-2),(1,-1),(1,2),(2,-2),(2,-1),(2,0),(2,1),(2,2) $ .

In the fourth test case, the pairs are $ (1,-2),(1,-1),(1,1),(1,2),(2,-2),(2,1),(2,2) $ .

这题实际上是一个建图题。原始的图 \(G\) 存在一些边的集合:\(E = \{(x \rightarrow x + a[x])\}\)。我们需要找到的是达到 \(\le 0 或者 \ge n\) 的位置。

定义 org 代表原始路径,org[i] 代表对应的节点。can 代表除了 org 外可以跳出去的节点的数量。

我们把他们分成两类节点,一类是 org 一类是除了 org 之外的节点。

我们通过 org_ok 来判断原始路径是否能直接跳出去。

如果 org_ok 的话,那么除了 org 外的所有节点都可以任意修改(每个节点都可以改 \(2\times n + 1\))。否则,我们不能随意修改。

对于 org 上的节点,我们只能考虑:

  • 首先是,任何节点都有 \(n+1\) 种直接跳出去的方式。
  • 除此之外,考虑如果 org_ok 可以直接跳到后面
  • 最重要的是,我们得考虑 can 中是否包含一些在 org_ok 情况下,跳回 org 路径的情况,那么我们需要避免修改一些路径导致回路。比如 2 -> 4 -> 3 -> -5 并且存在 x -> 2 的情况,如果你修改 4->x 看起来好像你跳到了一个可以出去的路径,实际上跑的是一个环。所以我们需要预处理 in papreSum 用来剔除这些(其实实现略麻烦)

最后看代码吧,WA 了 N 发。

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

// #define int long long
#define pb push_back
#define vt std::vector
#define rep(i, l, r) for (ll i = (l); i < (r); ++ i)
#define per(i, l, r) for (ll i = (l); i >= (r); -- i)

using ll = long long;
using pii = pair<int, int>;

template<typename... Args>
inline void _wpr_(Args... args) { std::cout << '\n'; }
template<typename T, typename... Args>
inline void _wpr_(T val, Args... args) { std::cout << " " << val; _wpr_(args...); }
template<typename T, typename... Args>
void wpr(T val, Args... args) { std::cout << val; _wpr_(args...); }

const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

vt<int> global_ans;
void solve(int th){
    int n;
    std::cin >> n;
    vt<int> a(n + 1);
    rep (i, 1, n + 1) 
        std::cin >> a[i];
    
    
    vt<int> vis(n + 1, 0), org(n + 1, 0), f(n + 1, -1), pa(n + 1, 0), in(n + 1, 0);
    iota(all(pa), 0); // init 每个位置等于自己
    int cnt = 0;
    function<int(int, int)> dfs = [&](int cur, int set_org){
        ++ cnt;
        if (set_org) 
            org[cur] = 1;
        int flag = 1, nxt = cur + a[cur];
        if (nxt >= 1 && nxt <= n){
            if (!vis[nxt]){
                vis[nxt] = 1;
                flag = dfs(nxt, set_org);
                if (!set_org) pa[cur] = pa[nxt]; // 更新前缀
                return f[cur] = flag;
            }
            else {
                if (f[nxt] == -1) return f[cur] = 0; // cycle
                // 一定不是第一次 set_org  why? 因为第一次要么时 vis[nxt] = 0 或者 找到了 cycle 
                flag = f[nxt]; // 之前已经计算过了
                if (org[pa[nxt]]){ // 如果 nxt 是连接到原始路线上的,我们就需要计算这个前缀
                    pa[cur] = pa[nxt]; // 更新
                    in[pa[cur]] += cnt; // 把整个路径上的节点都加到原始节点上
                }
            }
        }
        
        return f[cur] = flag;
    };

    int org_ok = 0; // 是否有原始路线
    int can = 0; // 除了原始路线之外,可以出去的节点数
    ll ans = 0, base = 0; // base: 原始路线的长度
    rep (i, 1, n + 1){
        if (i == 1){
            cnt = 0;
            vis[i] = 1;
            org_ok = dfs(i, 1);
            base = cnt;
        }else if (vis[i] == 0){
            cnt = 0;
            vis[i] = 1;
            int flag = dfs(i, 0);
            if (flag == 1) can += cnt;
        }
    }

    if (org_ok) // 如果有原始路线,那么除了原始路线之外的节点都可以随意设置
        ans += (n - base) * (2 * n + 1); 
    ll preSum = 0; // 计算到当前原始路径点 cr 时,有多少 can 里面的节点是跳到 [1 -> cr] 的路径上,这些点是不能用的!
    for (int cr = 1, i = 0; i < base; ++ i){
        ans += n + 1; // direct out (每个点都有 n + 1 种方法 直接出去)
        preSum += in[cr]; 

        // 这里是计算,借助不在原始路线上的节点,进行跳出的方法数
        if (org_ok) // 如果有原始路线,那么我们需要考虑 preSum 对 can 对影响
            ans += max(0ll, can - preSum);
        else ans += can; // 否则,所有的 can 都可以出去

        // 如果原始路线可以跳出,那么可以从 cr 节点直接往后面原始路径上的节点跳
        if (org_ok){
            ans += base - i - 1; // to nxt 
        }
        cr = cr + a[cr]; // 更新 cr
    }
    wpr(ans);
}

signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    global_ans.clear();
    std::cin >> t;
    for (int th = 1; th <= t; ++ th)
        solve(th);
    return 0;
}