Featured image of post Codeforces Round #1079(Div.2)

Codeforces Round #1079(Div.2)

最近的场好像风格奇怪了起来,感觉大部分都挺trivial的~

这个E1跟E2也不难,其实看着挺显然的,就是考验代码调试能力。

D刚好当天上午学了根号分治,晚上就考到了,我不好说,总之这把虽然很唐但是回蓝啦。

B

题目大意:给定一个长度为 $n$ 的排列 $p$ 和长度为 $n$ 的数组 $a$ ,可以对 $p$ 进行任意次以下操作: $p_i=p_{i+1}$ 或者 $p_{i+1}=p_i$ ,是否能把 $p$ 转变为 $a$ ?

数据范围: $2 \leq n \leq 2 \cdot 10^5,1 \leq p_i \leq n,1 \leq a_i \leq n$ 。

思路:手玩样例,不难发现如果能转变需要两个条件,一是需要 $a$ 中每个数的出现都要连续,二是相对 $p$ 中原来元素的出现顺序, $a$ 中的出现不能交错,也就是 $a$ 去重后是 $p$ 的子序列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void solve() {
    int n;
    cin >> n;
    vi p(n);
    vi a(n);
    rep(i, 0, n - 1) cin >> p[i];
    rep(i, 0, n - 1) cin >> a[i];
    vector<pair<int, int>> pos(n + 1, {INT_MAX, INT_MIN});
    vi cnt(n + 1);
    map<int, int> ma;
    rep(i, 0, n - 1) {
        pos[a[i]].first = min(pos[a[i]].first, i);
        pos[a[i]].second = max(pos[a[i]].second, i);
        cnt[a[i]]++;
        ma[p[i]] = i;
    }
    rep(i, 1, n) {
        if (pos[i] == make_pair(INT_MAX, INT_MIN)) continue;
        if (pos[i].second - pos[i].first + 1 != cnt[i]) {
            cout << "No" << endl;
            return;
        }
    }
    int l = 0;
    int r = 0;
    auto a2 = a;
    a2.erase(unique(all(a2)), a2.end());
    int m = sz(a2);
    rep(i, 0, m - 1) {
        if (ma[a2[i]] < l) {
            cout << "No" << endl;
            return;
        }
        l = max(l, ma[a2[i]]);
    }
    cout << "Yes" << endl;
    return;
}

C

题目大意:Alice和Bob在玩游戏,一开始给定两个数 $p$ 和 $q$ ,每次当 $p>0$ 可以将 $p$ 减一,当 $q>1$ 可以将 $q$ 减一。当 $p=0$ 且 $q=1$ 时,游戏结束。如果在任意时候 $\frac{p}{q}=\frac{2}{3}$ ,则Bob获胜,反之Alice获胜,请求出胜者。

数据范围: $1 \leq p,q \leq 10^18$

思路:手玩样例,发现当 $p-2x=q-3x$ 时,Bob可以操纵获得胜利,反之Alice获得胜利。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void solve() {
    ll p, q;
    cin >> p >> q;
    ll tot = p * 3;
    ll tot2 = p + q - 1LL;
    ll x = q - p;
    if ((tot % 2 == 0 && tot / 2 == q)  || (x > 0 && 1LL * 3 * x < q && 1LL * 2 * x < p)) {
        cout << "Bob" << endl;
        return;
    }
    cout << "Alice" << endl;
    return;
}

D

题目大意:给定数组 $a$ ,请求出 $a_i \cdot a_j=j-i$ 的下标对 $(i,j)$ 数量。

数据范围:$2 \leq n \leq 2 \cdot 10^5, 1 \leq a_i \leq 10^9$ 。

思路:这道题,由于左边的乘法式子并没有什么好的性质,因此考虑如何降低复杂度。

我的第一思路是,通过式子进行放缩然后求解,发现会被 $1,n,\ldots,1\ldots,1$ 卡掉。

然后,就开始瞪,发现可以用调和级数来枚举,但是还是被 $1,\ldots,1$ 卡掉。

刚好早上刚学过根号分治,于是就往这方面想了,这也是启发一个思路,就是根号复杂度是一个很好的复杂度(这句话不能给伊人看到,毕竟我还是黑子)。

可以看出当 $a_i$ 较大的时候,使用调和级数枚举右维护左,一次操作的复杂度为 $O(\frac{n}{a_i})$ 。

然后,当 $a_i$ 较小的时候,完全可以直接枚举一遍,操作复杂度为 $O(n)$ 。

设分界点为 $B$ ,有总操作复杂度为 $O(nB+(n-B) \cdot \frac{n}{B})$ 显然当 $B=\sqrt{n}$ 的时候取得最优。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void solve() {
    int n;
    cin >> n;
    vi a(n);
    rep(i, 0, n - 1) cin >> a[i];
    ll ans = 0;
    int B = sqrt(n);
    rep(i, 1, B - 1) {
        rep(j, 0, n - 1) {
            ll tem = 1LL * j + 1LL * i * a[j];
            if (tem < 0 || tem >= n) continue;
            if (a[tem] == i) ans++;
        }
    }
    rep(i, 0, n - 1) {
        ll tem = a[i];
        if (a[i] < B) continue;
        while (tem <= i) {
            ll tem2 = 1LL * i - tem;
            if (tem2 < 0) break;
            if (a[tem2] == tem / (1LL * a[i])) ans++;
            tem += 1LL * a[i];
        }
    }
    cout << ans << endl;
    return;
}

E1

题目大意:交互题,给定一个有向无环图,有 $n$ 个顶点和 $m$ 条边(这里 $m$ 是未知数),每次通过查询 $x$ 来找出按字典序排序的所有路径中的第 $x$ 条路径,请在 $32 \cdot (n+m)$ 次查询内找出这个图中所有的边。

数据范围: $n \leq 15$ 。

思路:首先考虑有向无环图的性质,根据前置知识知道它可以被拓扑排序,且拓扑序就是按照DFS/BFS进行的排序,因此按照字典序排序,就是a/a->b/a->b->c/b这样排的。

看到32其实会很自然地想到二分,这里可以直接按照路径的前两个点(一条新边)进行二分查找。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void solve() {
    int n;
    cin >> n;
    vector<pii> res;
    rep(i, 0, n - 1) {
        int las = -1;
        while (true) {
            int l = 1, r = (1 << 30), mid;
            int ans = r;
            while (l <= r) {
                mid = (l + r) / 2;
                vi tem = ask(mid);
                if (tem.empty())
                    r = mid - 1;
                else if (sz(tem) == 1 && tem[0] > i + 1)
                    r = mid - 1;
                else if (sz(tem) == 1 && tem[0] <= i + 1)
                    l = mid + 1;
                else if (sz(tem) >= 2) {
                    if (tem[0] < i + 1)
                        l = mid + 1;
                    else if (tem[0] > i + 1)
                        r = mid - 1;
                    else if (tem[0] == i + 1) {
                        if (tem[1] > las) {
                            r = mid - 1;
                            ans = mid;
                        } else
                            l = mid + 1;
                    }
                }
            }
            vi tem = ask(ans);
            if (sz(tem) <= 1) break;
            res.emplace_back(tem[0], tem[1]);
            las = tem[1];
        }
    }
    report(res);
    return;
}

E2

题目大意:跟上题一样,查询次数变为 $n+m$ 次,且 $n \leq 30$ 。

数据范围: $n \leq 30$ 。

思路:这里的查询次数变少了,因此肯定需要新的性质来做。

回想起,有向无环图可以在拓扑序上做DP,同时列举一下这里的字典序顺序可以发现,有很多需要跳过的部分,也就是遇到某个已经访问过点时需要跳过它的子树部分。

并且观察到,字典序相邻的两条路径,要么前者是后者的前缀(刚好增加一个顶点),要么删除一个顶点后缀,添加一个新的顶点后缀。

还没完,如果每次访问到一个已经访问的节点(指路径的最后一个顶点),直接跳过它的子树部分,那么每次访问,要么拓展一条新的边,要么跳到一个单顶点的路径,因此一共 $n+m$ 次,这里代码保证每次一定跳到单顶点路径,因此调的比较久。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void solve() {
    int n;
    cin >> n;
    vector<pii> res;
    vector<bool> pd(n, false);
    vl dp(n, 1);
    vi tem;
    ll cnt = 1;
    rep(i, 0, n - 1) {
        cnt += 1LL * dp[i];
        pd[i] = true;
        while (true) {
            tem = ask(cnt);
            if (sz(tem) <= 1) {
                break;
            }
            rep(j, 0, sz(tem) - 2) dp[tem[j] - 1] += dp[tem.back() - 1];
            res.emplace_back(tem[sz(tem) - 2], tem[sz(tem) - 1]);
            if (pd[tem.back() - 1]) {
                cnt += dp[tem.back() - 1];
                continue;
            }
            pd[tem.back() - 1] = true;
            cnt++;
        }
    }
    report(res);
    return;
}