第31次CCF计算机软件能力认证

发布时间 2023-09-17 20:02:23作者: ~Lanly~

100+100+100+100+60=460

坐标变换 (其一)

题目大意

给定\(n\)个操作,每个操作将坐标 \((x,y)\)变为 \((x + dx, y + dy)\)

给定 \(m\)个点,问这 \(m\)个点经过这 \(n\)次操作变换后的坐标。

解题思路

注意到操作是可合并的,因此可以先将这\(n\)个操作合并成一个操作,然后对每个点都经过这个操作变换即可,时间复杂度为 \(O(n + m)\)

本题 \(n,m\)只有 \(100\),也可以 \(O(nm)\)依次对每个点进行操作变换。

神奇的代码
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    int dx = 0, dy = 0;
    for(int i = 0; i < n; ++ i){
        int x, y;
        cin >> x >> y;
        dx += x;
        dy += y;
    }
    for(int i = 0; i < m; ++ i){
        int x, y;
        cin >> x >> y;
        cout << x + dx << ' ' << y + dy << '\n';
    }
    return 0;
}


坐标变换(其二)

题目大意

给定\(n\)个操作,操作分两类,一类将\((x, y)\)变为 \((kx, ky)\),一类进行坐标旋转,并贴心地给出了旋转公式。

给定 \(m\)个点,问这 \(m\)个点经过第\(l\)次到第\(r\)次操作变换后的坐标。

解题思路

直接模拟的\(O(nm)\)可以拿 \(80\)分 。

考虑操作合并,因为在第二类操作里,两维会相互影响,不能像第一题单纯的合并,但由于是线形的,可以写成一个矩阵乘法的方式,因此可以写成矩阵乘法的形式,用线段树维护区间乘,当然也可以分块维护区间操作,但想到第二题应该不会有这么复杂的写法,于是赶紧冷静下来想想有什么其他的写法。

注意到一个操作是改变与原点的距离,一个操作是改变与\(x\)轴所夹成的角度,如果考虑坐标在极坐标系下的表示形式,会发现这两种操作只是分别对其中一维进行操作,且这些操作是可逆的,且不会相互影响。

因此我们就预处理出 \(op[i]\)表示操作 \(1..i\)对距离 \(r\)和角度 \(\theta\)的影响,这是一个前缀和数组。

然后对于一个点问经过操作 \(l..r\)的结果,先对它施加\(1..r\)操作的影响,再消除 \(1..l-1\)操作的影响,即可得到 \(l..r\)操作的结果。

施加影响,就是长度 \(\times k\),角度 \(+ \theta\),消除影响,就是长度 \(/ k\),角度 \(- \theta\)

最后根据\(r\)\(\theta\)还原出\(x = r\cos \theta, y = r\sin \theta\)

时间复杂度为 \(O(n + m)\)

神奇的代码
#include <bits/stdc++.h>
using namespace std;

const double pi = acos(-1);

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<array<double, 2>> op(n);
    for(auto &i : op){
        int opp;
        double k;
        cin >> opp >> k;
        if (opp == 1)
            i[0] = k;
        else{
            i[0] = 1;
            i[1] = k;
        }
    }
    for(int i = 1; i < n; ++ i){
        op[i][0] *= op[i - 1][0];
        op[i][1] += op[i - 1][1];
    }
    for(int i = 0; i < m; ++ i){
        int l, r, x, y;
        cin >> l >> r >> x >> y;
        -- l, -- r;
        double R = sqrt(1ll * x * x + 1ll * y * y), theta = 0;
        if (x == 0){
            if (y > 0)
                theta = pi / 2;
            else 
                theta = -pi / 2;
        }else{
            theta = atan2(y, x);
        }
        R *= op[r][0];
        theta += op[r][1];
        if (l){
            R /= op[l - 1][0];
            theta -= op[l - 1][1];
        }
        cout << fixed << setprecision(10) << R * cos(theta) << ' ' << R * sin(theta) << '\n';
    }
    return 0;
}


梯度求解

题目大意

给定一个仅包含\(+-*\)运算的多元函数,求关于 \(x_i\)在点 \((a_1,a_2,...)\)处的偏导值。

函数以逆波兰形式给出。

解题思路

感觉有意思的题,没接触过,琢磨了许久。

首先我们不能以人的方式考虑对\((x^3)^\prime = 3x^2\)这样的形式的求导,而是像例子给看成的\(x_1 \cdot x_1 \cdot x_1\)用乘法求导的式子计算。这样每个的原子项都是形如 \(x_i\)或者常数这样最简形式。虽然可能计算不如上述的简单,但毕竟是机器算,数据范围也比较小,就尽可能简单一点。

不合并同类项后,考虑对这个表达式的求导,我们尽可能要找一个可以递归的结构进行求导,这个结构其实跟表达式计算是一样的。

通过逆波兰表达式建立这个表达式树,如下图所示,题面也贴心地指出了逆波兰表达式就是这棵树的后序遍历。

表达式树

树就是一棵有递归结构的表示形式,我们要求整棵树的偏导值,先看根节点的运算符,比如是乘号,由于\((uv)^\prime = u^\prime v + uv^\prime\),那我们需要求左子树的函数值和偏导值,右子树的函数值和偏导值,然后根据上述求导的式子计算得到当前子树的函数值和偏导值。而求左右子树的函数值和偏导值实际上就是一个子问题,因此就可以递归下去了。如果是加法的话就是偏导值相加。

边界情况就是当前节点是常数或者变量时,根据求导式子可以直接得到函数值和偏导值

其实最后发现和表达式计算没什么区别。时间复杂度为\(O(nm)\)

注意减法不满足交换律,在建立表达式树时不要搞错运算符左右两个值的顺序。

考场上模数习惯性写成\(998244353\)找了好久的错误。

神奇的代码
#include <bits/stdc++.h>
using namespace std;

const int mo = 1e9+7;
#define CONST -1
#define VAR -2
#define OP -3

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    string s;
    int n, m;
    cin >> n >> m;
    getline(cin, s); // '\n'
    getline(cin, s);
    istringstream qwq(s);
    vector<int> l;
    vector<int> r;
    vector<int> info;
    vector<int> kind;
    stack<int> id;
    int node_id = 0;
    while(getline(qwq, s, ' ')){
        if (s.size() == 1 && (s[0] == '+' || s[0] == '*' || s[0] == '-')){
            int rson = id.top();
            id.pop();
            int lson = id.top();
            id.pop();
            l.push_back(lson);
            r.push_back(rson);
            info.push_back(s[0]);
            kind.push_back(OP);

            id.push(node_id);
            ++ node_id;
        }else if (s[0] == 'x'){
            int x = stoi(s.substr(1));
            -- x;
            l.push_back(-1);
            r.push_back(-1);
            info.push_back(x);
            kind.push_back(VAR);

            id.push(node_id);
            ++ node_id;
        }else{
            int x = stoi(s);
            l.push_back(-1);
            r.push_back(-1);
            info.push_back(x);
            kind.push_back(CONST);

            id.push(node_id);
            ++ node_id;
        }
    }
    int root = id.top();
    vector<int> a(n);

    function<array<int, 2>(int, int)> solve = [&](int u, int x){
        if (kind[u] == VAR){
            return array<int, 2>{a[info[u]], (info[u] == x)};
        }else if (kind[u] == CONST){
            return array<int, 2>{info[u], 0};
        }else{
            auto lans = solve(l[u], x), rans = solve(r[u], x);
            int sum = 0, dsum = 0;
            if (info[u] == '+'){
                sum = lans[0] + rans[0];
                dsum = lans[1] + rans[1];
                if (sum >= mo)  sum -= mo;
                if (dsum >= mo) dsum -= mo;
            }else if (info[u] == '-'){
                sum = lans[0] - rans[0];
                dsum = lans[1] - rans[1];
                if (sum >= mo)  sum -= mo;
                if (dsum >= mo) dsum -= mo;
            }else{
                sum = 1ll * lans[0] * rans[0] % mo;
                dsum = (1ll * lans[0] * rans[1] % mo + 1ll * lans[1] * rans[0] % mo);
                if (dsum >= mo) dsum -= mo;
            }
            if (sum < 0)
                sum += mo;
            if (dsum < 0)
                dsum += mo;
            return array<int, 2>{sum, dsum};
        }
    };
    for(int i = 0; i < m; ++ i){
        int x;
        cin >> x;
        -- x;
        for(auto &i : a)
            cin >> i;
        cout << solve(root, x)[1] << '\n';
    }
    return 0;
}


阴阳龙

题目大意

二维平面\(n \times m\),有\(p\)个人,位于不同坐标。

\(q\)次事件\((x,y,t)\)发生, 每次事件,在\((x,y)\)上会有阴阳龙出现,从该位置的周围的8个方向中,找到切比雪夫距离最小的那些人,它们的位置将进行 \(t\)倍的 \(\frac{1}{8}\)圆的逆时针旋转。注意如果这个最小距离大于该位置\((x,y)\)到边界的切比雪夫距离,则无事发生(因为旋转的话可能会飞到区域外了)。没有人也无事发生。

问这 \(m\)次事件后,所有人的坐标。

解题思路

考虑暴力,对于某个事件,从\((x,y)\)依次往 \(8\)个方向递增,直到碰到人后,暴力旋转。时间复杂度为 \(O(max(n,m)q)\)

注意到人的坐标互不相同,那么每个事件最多只会更改\(8\)个人的坐标,如果我们能快速找到这 \(8\)个人,复杂度就是 \(O(8q)\)

考虑如何快速找到,对于同一行的人中,我们用set储存它们的列坐标,这样通过\(lower\_bound\)就可以在\(O(\log)\)的事件内找到 \((x,y)\) 最小的大于\(y\)和最大的小于 \(y\)的人,作为候选者。

同理对于同一列,同一对角线都这么找。同一行的人,它们的 \(x\)相同。同一列的人,它们的 \(y\)相同。同一对角线的人,它们的 \(x+y\)\(x-y\)是相同的。

因此就开\(row[x], col[y], ld[x + y], rd[x - y]\)表示第\(x\)行的所有人的 \(y\)集合,第 \(y\)列的所有人的 \(x\)集合,左上\(\to\)右下的对角线满足 \(x+y\)的所有人的\(y\)集合,以及左下\(\to\)右上的对角线满足 \(x-y\)的所有人的 \(y\)集合。

对于当前的阴阳龙位置 \((x,y)\),分别花\(O(\log)\) 时间找到八个方向的最近 的候选人,然后根据距离排个序,得到最近的那些人,再进行题意所述的坐标变换即可。

时间复杂度是\(O(q\log p)\)

神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

const int dx[8] = {1, 1, 0, -1, -1, -1, 0, 1};
const int dy[8] = {0, 1, 1, 1, 0, -1, -1, -1};

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m, p, q;
    cin >> n >> m >> p >> q;
    vector<array<int, 2>> pos(p);
    unordered_map<int, set<array<int, 2>>> row, col, ld, rd;
    auto insert = [&](int id){
        int x = pos[id][0], y = pos[id][1];
        row[x].insert({y, id});
        col[y].insert({x, id});
        ld[x + y].insert({y, id});
        rd[x - y].insert({y, id});
    };
    auto remove = [&](int id){
        int x = pos[id][0], y = pos[id][1];
        row[x].erase({y, id});
        col[y].erase({x, id});
        ld[x + y].erase({y, id});
        rd[x - y].erase({y, id});
    };
    for(int i = 0; i < p; ++ i){
        cin >> pos[i][0] >> pos[i][1];
        insert(i);
    }
    for(int i = 0; i < q; ++ i){
        int u, v, t;
        cin >> u >> v >> t;
        vector<array<int, 3>> candidate;

        auto search = [&](const set<array<int, 2>>& people, int d, int dirr, int dirl){
            auto pos = people.lower_bound(array<int, 2>{d, p});
            if (pos != people.end()){
                candidate.push_back({(*pos)[0] - d, (*pos)[1], dirr});
            }
            if (pos != people.begin()){
                pos = prev(pos);
                if ((*pos)[0] == d && pos != people.begin())
                    pos = prev(pos);

                if ((*pos)[0] != d){
                    candidate.push_back({d - (*pos)[0], (*pos)[1], dirl});
                }
            }
        };

        search(row[u], v, 2, 6);
        search(col[v], u, 0, 4);
        search(ld[u + v], v, 3, 7);
        search(rd[u - v], v, 1, 5);

        if (candidate.empty())
            continue;
        sort(candidate.begin(), candidate.end(), [&](const array<int, 3>& a, const array<int, 3>& b){
            return a[0] < b[0];
        });
        int mindis = min({u - 1, n - u, v - 1, m - v});
        if (candidate[0][0] > mindis)
            continue;
        mindis = candidate[0][0];
        for(int i = 0; i < candidate.size(); ++ i){
            if (candidate[i][0] != mindis)
                break;
            int dis = candidate[i][0];
            int id = candidate[i][1];
            remove(id);
            int dir = (candidate[i][2] + t) % 8;
            pos[id][0] = u + dis * dx[dir];
            pos[id][1] = v + dis * dy[dir];
            insert(id);
        }
    }
    LL ans = 0;
    for(int i = 0; i < p; ++ i){
        ans ^= (1ll * (i + 1) * pos[i][0] + pos[i][1]);
    }
    cout << ans << '\n';
    return 0;
}


阻击

题目大意

给定一棵树,边有边权,\(m\)次修改,每次修改一个边权。

问每次修改后树的直径长度。

  • 性质A,一个链。
  • 性质B,边权非负。
  • 性质C,任意两点的路径边数不超过\(100\)

解题思路

树的直径经典求法就是\(O(n)\)的两次 \(BFS\)\(DFS\), 但有个前提是边权必须非负。而本题并不保证这一性质,只有数据里的性质B有这个特征。

尽管如此,注意到树的递归特性,直径要么在某个子树里,要么横跨了根节点,而后者是两条到根节点最长的路径拼接起来的。这非常类似分治的想法。

\(maxd[u]\)表示在\(u\)的子树中到节点\(i\)的最长距离,在分治求解过程中,先求其儿子子树\(v\)的答案,然后求最大值\(max(maxd[u] + maxd[v] + d[u][v])\),同时更新\(maxd[u]\)的值。

这样同样\(O(n)\)就可以得出直径长度。

时间复杂度是 \(O(nm)\),有 \(48\)分。

考虑性质 \(A\)的链的做法,就是问一个区间的最大子区间和。这是一个经典问题,同样考虑分治,从中间切开后,答案要么在左右子区间里,要么横跨左右子区间,前者递归求解,后者则是从中间往左右延伸的最大和,这个 \(O(n)\)就能求解。

考虑实时维护信息,注意到这些信息是可合并的,也即一个大区间的答案可以由两个小区间的信息合并得到,因此我们用线段树维护这些区间,维护的信息包括:

  • 区间答案
  • 区间和
  • 前缀最大值
  • 后缀最大值

这样就可以得到大区间的这四个信息,就可以实时维护答案了。每次修改重新求答案的复杂度是 \(O(\log n)\)。总的时间复杂度是 \(O(m\log n)\)。这有\(12\)分。

到这里就\(60\)分了。

考虑性质 \(C\),还是考虑上述树形态的做法,在边权修改时如何实时维护需要的信息。注意到合并答案时是根据子树的答案和子树的\(maxd\)进行的,我们需要实时维护这些值,当一条边权修改时,从这条边的点一路往父亲方向走的所有点的信息都会改变,由于性质\(C\),这样的点最多 \(100\)个。因此每次修改重新求答案的时间复杂度是 \(O(100)\),总的时间复杂度是 \(O(100m \log n)\),这里的\(\log\)是要维护子树的 \(maxd\)的最大的两个值。时间上是能通过,但一直 \(WA\),没找到原因,没拿到这\(12\)分。

至于性质 \(B\), 没想到有什么用。

至于正解,不会,只会对着性质拿分了。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

const int N = 1e5 + 8;

class segment{
    #define lson root << 1
    #define rson root << 1 | 1
    LL ans[N << 2];
    LL lsum[N << 2];
    LL rsum[N << 2];
    LL sum[N << 2];

    public:
    void popup(int root){
        ans[root] = max({ans[lson], ans[rson], rsum[lson] + lsum[rson]});
        lsum[root] = max(lsum[lson], sum[lson] + lsum[rson]);
        rsum[root] = max(rsum[rson], sum[rson] + rsum[lson]);
        sum[root] = sum[lson] + sum[rson];
    }

    void build(int root, int l, int r, vector<int>& a){
        if (l == r){
            ans[root] = (a[l] >= 0 ? a[l] : 0);
            lsum[root] = (a[l] >= 0 ? a[l] : 0);
            rsum[root] = (a[l] >= 0 ? a[l] : 0);
            sum[root] = a[l];
            return;
        }
        int mid = (l + r) >> 1;
        build(lson, l, mid, a);
        build(rson, mid + 1, r, a);
        popup(root);
    }

    void update(int root, int l, int r, int pos, int val){
        if (l == r){
            ans[root] = (val >= 0 ? val : 0);
            lsum[root] = (val >= 0 ? val : 0);
            rsum[root] = (val >= 0 ? val : 0);
            sum[root] = val;
            return;
        }
        int mid = (l + r) >> 1;
        if (pos <= mid)
            update(lson, l, mid, pos, val);
        else 
            update(rson, mid + 1, r, pos, val);
        popup(root);
    }

    LL query(int root){
        return ans[root];
    }
}seg;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<vector<int>> G(n);
    vector<array<int, 4>> edge;
    for(int i = 1; i < n; ++ i){    
        int u, v, w, b;
        cin >> u >> v >> w >> b;
        -- u, -- v;
        G[u].push_back(edge.size());
        G[v].push_back(edge.size());
        edge.push_back({u, v, w, b});
    }
    vector<LL> maxd(n, 0);
    LL ans = 0;
    function<void(int, int)> dfs = [&](int u, int fa){
        for(auto &i : G[u]){
            int v = edge[i][0] ^ edge[i][1] ^ u;
            if (v == fa)
                continue;
            int d = edge[i][3] - edge[i][2];
            dfs(v, u);
            ans = max(ans, maxd[u] + maxd[v] + d);
            maxd[u] = max(maxd[u], maxd[v] + d);
        }
    };
    dfs(1, 1);
    cout << ans << '\n';
    if (m < 100000){
        for(int i = 1; i <= m; ++ i){
            int x, y;
            cin >> x >> y;
            -- x;
            edge[x][2] = y;
            ans = 0;
            fill(maxd.begin(), maxd.end(), 0);
            dfs(1, 1);
            cout << ans << '\n';
        }
    }else{
        int ok1 = true;
        for(int i = 0; i < n - 1; ++ i){
            ok1 &= (edge[i][0] == i && edge[i][1] == i + 1);
        }
        if (ok1){
            // A
            vector<int> a(n);
            for(int i = 1; i < n; ++ i){
                a[i] = edge[i - 1][3] - edge[i - 1][2];
            }
            seg.build(1, 1, n - 1, a);
            for(int i = 1; i <= m; ++ i){
                int x, y;
                cin >> x >> y;
                -- x;
                edge[x][2] = y;
                seg.update(1, 1, n - 1, x + 1, edge[x][3] - edge[x][2]);
                cout << seg.query(1) << '\n';
            }
        }else{
            // C
            vector<set<array<LL, 2>>> anss(n), maxs(n);
            vector<unordered_map<int, LL>> anss_id(n), maxs_id(n);
            vector<int> deep(n), f(n);
            function<void(int, int)> dfs2 = [&](int u, int fa){
                f[u] = fa;
                int leave = true;
                for(auto &i : G[u]){
                    int v = edge[i][0] ^ edge[i][1] ^ u;
                    if (v == fa)
                        continue; 
                    leave = false;
                    deep[v] = deep[u] + 1;
                    dfs2(v, u); 
                    int d = edge[i][3] - edge[i][2];

                    LL ans_val = (*anss[v].rbegin())[0];
                    anss[u].insert({ans_val, v});
                    anss_id[u][v] = ans_val;


                    LL maxs_val = (*maxs[v].rbegin())[0] + d;
                    maxs[u].insert({maxs_val, v});
                    maxs_id[u][v] = maxs_val;
                }             
                anss[u].insert({0, -1});
                maxs[u].insert({0, -1});

                if (maxs[u].size() > 1){
                    auto c1 = maxs[u].rbegin();
                    auto c2 = next(c1);
                    anss[u].insert({(*c1)[0] + (*c2)[0], -2});
                    anss_id[u][-2] = (*c1)[0] + (*c2)[0];
                }
            };
            dfs2(0, -1);
            for(int i = 0; i < m; ++ i){
                int x, y;
                cin >> x >> y;
                -- x;
                edge[x][2] = y;
                int d = edge[x][3] - edge[x][2];
                ans = 0;
                int cur = (deep[edge[x][0]] < deep[edge[x][1]] ? edge[x][0] : edge[x][1]);
                int son = cur ^ edge[x][0] ^ edge[x][1];
                while(cur != -1){
                    maxs[cur].erase({maxs_id[cur][son], son});
                    maxs_id[cur][son] = (*maxs[son].rbegin())[0] + d;
                    maxs[cur].insert({maxs_id[cur][son], son});

                    anss[cur].erase({anss_id[cur][son], son});
                    anss_id[cur][son] = (*anss[son].rbegin())[0];
                    anss[cur].insert({anss_id[cur][son], son});

                    anss[cur].erase({anss_id[cur][-2], -2});
                    if (maxs[cur].size() > 1){
                        auto c1 = maxs[cur].rbegin();
                        auto c2 = next(c1);
                        anss_id[cur][-2] = (*c1)[0] + (*c2)[0];
                        anss[cur].insert({anss_id[cur][-2], -2});
                    }

                    son = cur;
                    cur = f[cur];
                }
                ans = max(0ll, (*anss[0].rbegin())[0]);
                
                cout << ans << '\n';
            }

        }
    }
    return 0;
}