算法杂记 2023/02/26
算法杂记 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
pa
和preSum
用来剔除这些(其实实现略麻烦)
最后看代码吧,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;
}