AtCoder Beginner Contest 355 (E,F)

总结:

这把B都错题了一直Wa,然后队友告诉我说F貌似可做,写了半个小时F发现题目读假了,于是四题下班。


E - Guess the Sum

  1. 题目大意:
  • 给定一个隐藏的、长度为N的数组,下标从0开始,题目给定L,R,要你用最少的询问次数求出\(\sum_{i = L}^{R}a_{i}\)
  • 对于每次询问,可以选择一个 i 和 j ,然后询${\textstyle\sum_{l}^{r}}ai(l = 2 ^ i * i, r = 2 ^ i * (j + 1)) $ 。
  1. 思路分析:
  • 一开始我想的是从起点开始,对于每个点枚举最大能够整除的$2^{i} $,然后直接能跳就跳,然后我Wa了,不知道为啥,当时做的时候,没想过还能够把当前的左端点变小的。然后就没思路了,去找别人的题解了。
  • 假设长度为3,现在我要问 \([1, 7]\), 如果以我之前的思路,那答案会这么变化 $(1, 2), (2, 3), (4, 7) $, 这样问了3次,但能不能更优呢?
  • 其实可以先问 \([0, 8)\),再问\([0, 1)\),然后把0给减掉就可以了,这就有点奇怪了,既然能往回走,就很难去想个策略去解决这个问题。题目最多只有\(2 ^ {N}\)个点,所以我们可以尝试去搜索,整张图只有\(N\cdot 2^{N}\)条边,所以建图然后从起点广搜就可以了。然后对于每个点记录前驱就可以找到整个路径了。然后建图的时候对于每个点只能走\(2 ^ {i}\),所以直接枚举次数,像 $ 2 ^ {1} - 2 ^ {2} - 2 ^ {3}$这么连边就可以了.
  1. 代码:
void solve() {
    int N, L, R;cin >> N >> L >> R;
    int up = 1 << N;
    vvi g(up + 2, vi());
    for(int i = 0; i <= N; i++) {
        for(int l = 0; l < up; l += (1 << i)) {
            int r = l + (1 << i);
            g[l].push_back(r);
            g[r].push_back(l);
        }
    }
    queue<int> q;
    vi pre(up + 1, -1);
    vpi query;
    q.push(L);
    pre[L] = 0;
    while(!q.empty()) {
        int t = q.front();
        q.pop();
        if(t == R + 1) break;
        for(auto v : g[t]) {
            if(pre[v] != -1) continue;
            pre[v] = t;
            q.push(v);
        }
    }

    for(int i = R + 1; i != L; i = pre[i]) {
        query.push_back({pre[i], i});
    }
    reverse(query.begin(), query.end());
    int sum = 0;
    for(auto [l, r] : query) {
        int flag = l > r ? -1 : 1;
        if(flag == -1) swap(l, r);
        int i = __builtin_ctz(r - l);
        int j = l >> i;
        cout << "? " << i << " " << j << endl;
        int t;cin >> t;
        sum += t * flag;        
    }
    sum = (sum % 100 + 100) % 100;
    cout << "! " << sum << endl;
}

F - MST Query

  1. 题目大意:
    给你一张 \(n\) 个点和 \(n - 1\) 条边的无向连通图。给定 \(Q\) 次查询,每次查询给定一条边,问添加这条边之后整张图的最小生成树的大小是多少,每次查询不独立。
  2. 思路分析:
  • 我们发现,如果每次查询时独立的,那么直接用树剖就可以解决。但是每次查询不是独立的,这就意味着每次查询添加的边会一直存在,但是由于边的大小非常特殊,题目保证 $ W_{i} \le 10$ ,所以可以考虑别的做法。
  • 考虑开十张图,第 \(i\) 张图里面所有的连边的边权都是小于等于\(i\)的,考虑使用一个\(cnt\)数组,\(cnt_{i}\) 表示用所有 $ Wi \le i$ 的边可以让第 \(i\) 张图有几个连通块
  • 当每次添加进来一条长度为 $ w $ 的边,那么这条边只会影响所有边权大于等于 $ w $ 的图,然后判断每一张图中的$ (u, v) $ 是否有连边可以直接用10个并查集判断即可。
  • 最后查找答案的时候,直接用当前剩余的连通块数减去 \(cnt_{i}\) 就可以知道边权为 \(i\) 的边用了几次了,然后直接累加所有的边权就可以了。
  1. 代码:
struct DSU {
    std::vector<int> f, siz;
    
    DSU() {}
    DSU(int n) {
        init(n);
    }
    
    void init(int n) {
        f.resize(n);
        std::iota(f.begin(), f.end(), 0);
        siz.assign(n, 1);
    }

    int find(int x) {
        while (x != f[x]) {
            x = f[x] = f[f[x]];
        }
        return x;
    }
    
    bool same(int x, int y) {
        return find(x) == find(y);
    }
    
    bool merge(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            return false;
        }
        siz[x] += siz[y];
        f[y] = x;
        return true;
    }
    
    int size(int x) {
        return siz[find(x)];
    }
};
void solve() {
    int n, q;cin >> n >> q;
    vi cnt(11, n);
    vector<DSU> dsu(11, DSU(n + 1));
    for(int i = 1; i <= n - 1; i++) {
        int u, v, w;cin >> u >> v >> w;
        for(int j = w; j <= 10; j++) {
            cnt[j] -= dsu[j].merge(u, v);
        }
    }
    while(q--) {
        int u, v, w;cin >> u >> v >> w;
        ll lst = n, ans = 0;
        for(int i = w; i <= 10; i++) {
            cnt[i] -= dsu[i].merge(u, v);
        }
        for(int i = 1; i <= 10; i++) {
            ans += i * (lst - cnt[i]);
            lst = cnt[i];
        }
        cout << ans << "\n";
    }
}
posted @ 2024-06-01 17:30  orzkeyhacker  阅读(32)  评论(0编辑  收藏  举报