2023CCPC哈尔滨站 9题 题解(附代码)

哈尔滨之旅体验很棒, 本次CCPC哈尔滨是我队第一次参加(i/c)cpc,为纪念此行,回来补了4题,发一篇9题 题解。

可以在UCUP提交:2023CCPC哈尔滨站

不知何时能做cf上放送,如果这份题解对您有帮助,求个赞。

B. Memory 


题意 : 给一个数组a,求每一个Mood(i)的正负性,Mood(i)的定义如图

题解 :是签到题,注意有递推式:Mood(i)=Mood(i-1)/2 + a[i],猜都能猜到你要是暴力算肯定是精度就消失了,准WA,但是这个/2就启发我们可以往二进制的方向考虑,随之就可以发现每次+a[i]对小数位的影响是直接刷新,而没有对小数位有运算(我们只为了判断正负号),所以就开两个变量x表示整数部分,Dec表示小数部分,用递推式模拟即可,复杂度O(n)。

代码 : 

int main() {
    int n;
    std::cin >> n;

    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }

    std::string res;

    int x = 0, Dec = 0;
    // 整数位 x 和 小数位符号 Dec

    for (int i = 0; i < n; i++) {
        x += a[i];

        if (x != 0)
            res.push_back(x > 0 ? '+' : '-');
        else {
            if (Dec != 0)
                res.push_back(Dec > 0 ? '+' : '-');
            else
                res.push_back('0');
        }

        if (std::abs(x) & 1)
            Dec = (x > 0 ? 1 : -1);
        x /= 2;
    }

    std::cout << res;
}

C. Karshilov's Matching Problem II

题意 

1<= n,m <= 150000, 0<= w_i <= 10^8

题解:首先我们思考一个暴力,预处理出w数组的前缀和Sw,然后用z函数预处理出T的每个后缀跟S的lcp,记为z[i]好了,那么一次询问的答案就是\sum_{i=l}^r Sw[\min(r-i+1, z[i])], 直接暴力肯定会TLE,我们考虑把min函数拆开,分开计算,或许能优化一波。

于是我们尝试对于每个询问, 找到最左的mid, 满足l\le mid\le r使得 [mid, r]就是S的一个前缀,那么这样的话,大于等于mid的 i 在上述的min函数里就都取(r-i+1), 小于mid的 i 在上述的min函数里就都取z[i],这两个方向基于预处理, 都可以O(1)回答,其中小于mid的部分很好做,就用Sw数组在z[]上走一个前缀和即可;其中大于等于mid的部分不太好做,笔者选择结合KMP来预处理,每次只考虑以 i 为右端点的子串的贡献,最后再来个前缀和即可。 至于找mid的方法,笔者选择的是线段树上二分。复杂度O((n+m)logn),题解说此题还有O(n*sqrt(n))的莫队做法,但是笔者想了两天也没想出来,球球会的hxd在评论区教教。

代码 

using i64 = long long;

// Z函数
auto Zalgo(const std::string &s) {
    int n = s.size();
    std::vector<int> z(n);
    for (int i = 1, l = 0, r = 0; i < n; i++) {
        if (i < r)
            z[i] = std::min(z[i - l], r - i);
        while (i + z[i] < n and s[i + z[i]] == s[z[i]])
            z[i]++;
        if (i + z[i] > r)
            l = i, r = i + z[i];
    }
    return z;
}

constexpr int N = 1.5e5 + 20;

int t[N << 2];
// 线段树维护区间最大值
void up(int i) {
    t[i] = std::max(t[i << 1], t[i << 1 | 1]);
}

void modify(int i, int l, int r, int x, int y) {
    if (l == r) {
        t[i] = y;
        return;
    }

    int mid = l + r >> 1;
    if (x <= mid)
        modify(i << 1, l, mid, x, y);
    else
        modify(i << 1 | 1, mid + 1, r, x, y);
    up(i);
}

// 线段树上二分找到 [tl, tr] 里最左边 ≥y 的下标
int res = 0;
void get(int i, int l, int r, int tl, int tr, int y) {
    if (res < tr + 1 or t[i] < y)
        return;
    int mid = l + r >> 1;
    if (tl <= l and r <= tr) {
        if (l == r) {
            res = l;
            return;
        }
        get(i << 1, l, mid, tl, tr, y);
        get(i << 1 | 1, mid + 1, r, tl, tr, y);
        return;
    }

    if (tl <= mid)
        get(i << 1, l, mid, tl, tr, y);
    if (mid < tr)
        get(i << 1 | 1, mid + 1, r, tl, tr, y);
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

    std::string S, T;
    std::cin >> S >> T;

    std::vector<int> w(n + 1);
    std::vector<i64> sw(n + 1);
    for (int i = 1; i <= n; i++) {
        std::cin >> w[i];
        sw[i] = sw[i - 1] + w[i];
    }

    auto Z0 = Zalgo(S + "0" + T);
    std::vector<int> zt(Z0.begin() + n + 1, Z0.end());

    for (int i = 0; i < n; i++) {
        modify(1, 0, n - 1, i, zt[i] + i - 1);
        // 维护从 T[i] 开始匹配 pre(S) 最远匹配到的下标
    }

    auto Find = [&](int l, int r, int y) {
        // 找到区间[l, r]里匹配右端点 ≥y 的最左的下标
        res = r + 1;
        get(1, 0, n - 1, l, r, y);
        return res;
    };

    std::vector<int> link(n, 0);
    std::vector<i64> pre(n + 1, 0);

    for (int i = 0; i <= n; i++)
        pre[i] = w[i]; // 首先前缀本身需要算进答案

    // KMP 求pre[i] : f(pre[i])
    for (int i = 1, j = 0; i < n; i++) {
        while (j and S[i] != S[j])
            j = link[j - 1];
        j += (S[i] == S[j]);
        link[i] = j;
        pre[i + 1] += pre[j];
    }
    for (int i = 1; i <= n; i++)
        pre[i] += pre[i - 1];

    std::vector<i64> L(n + 1, 0); // 左段的贡献(前缀和)
    for (int i = 1; i <= n; i++)
        L[i] = L[i - 1] + sw[zt[i - 1]];

    int l, r;
    while (m--) {
        std::cin >> l >> r;
        --l, --r;
        int mid = Find(l, r, r);
        i64 ans = L[mid] - L[l] + +pre[r + 1 - mid];
        std::cout << ans << '\n';
    }
}

D. A Simple MST Problem

题意:定义w(x) 为x的质因子集合的大小,比如w(9)=1。每次询问给一个l,r 然后需要求出如果一个图只有l,l+1, ...r 这些点,点x和点y之间的边权是w(lcm(x,y)), 求最小生成树边权和。

题解:大家都知道 w(lcm(x, y)) = w(x) + w(y) - w(gcd(x, y))

首先考虑如果区间里有w(x) = 1的数,即x = p^k, 那么我们每个点 y 都跟 x 连边,这样的边权和就蛮小的,因为每条边最多w(y)+1,所以我们思考一个点 y 需要和谁连边的时候,只需要考虑w(y)和w(y)+1的边权即可,w(y)+1的非常好办,只需要跟w(x) = 1的 x 连边即可,w(y) 的边权需要 y 和质因子集合是 y 的子集的数 u 连边,可是这样的 u 有好多个,不仅枚举不来,就算连完了,跑mst都肯定TLE了,怎么办? 那么我们就只尝试连接离 y 最近的 u 就行,所以我们预处理出两个东西

1,距离 y 最近的小于 y 的 u,满足 u 的质因子集合是 y 的质因子集合的子集。

2,距离 y 最近的大于 y 的 u,满足 u 的质因子集合是 y 的质因子集合的子集。

然后我们每个数 y 都和左右边的 u 各连一条边权为 w(y) 的边,再和一个固定的 x 连一条边(注意 x 满足w(x) = 1),共3*n条边。最后Kruskal求mst即可, 总时间复杂度O(nlogn) 。

考虑如果区间里没有w(x) = 1的数,区间肯定很小,暴力即可。

但是笔者的做法并不优,所以需要卡常,比如求上述两个 u 的时候需要用到一个函数proP(x)表示x的质因子集合的元素乘积,这里w(x)和proP(x)都是积性函数, 可以线性筛,这里的mst用Kruskal求的时候,因为边权数值很小,可以考虑计数排序, 这样单次询问的复杂度就降到了O(r-l+1)。

代码

// 简单的一个并查集类
struct DSU {
    std::vector<int> f;
    std::vector<int> size;

    DSU(int n) : f(n), size(n) {
        std::iota(f.begin(), f.end(), 0);
        std::fill(size.begin(), size.end(), 1);
    }

    int find(int x) { // 路径压缩
        while (x != f[x])
            x = f[x] = f[f[x]];
        return x;
    }

    void Union(int x, int y) {
        if (find(x) == find(y))
            return;
        if (size[x] < size[y]) // 按秩合并
            std::swap(x, y);

        size[find(x)] += size[find(y)];
        f[find(y)] = find(x);
    }
};

constexpr int N = 1e6 + 2;

int P[N], w[N], proP[N], v[N], tot;
// proP[x]是 x 的所有质因子的乘积
int L[N], R[N], cnt[N];
int CappsPre = [] {
    v[1] = 1;
    std::fill(proP, proP + N, 1);
    // 为了卡常, 最好是把 w 和 proP 都用线性筛求
    // 毕竟这里的 w 和 proP 函数都是积性函数
    for (int i = 2; i < N; i++) {
        if (v[i] == 0) {
            P[++tot] = i;
            v[i] = i;
            w[i] = 1;
            proP[i] = i;
        }
        for (int j = 1; j <= tot and i * P[j] < N; j++) {
            v[i * P[j]] = P[j];
            if (i % P[j] == 0) {
                w[i * P[j]] = w[i];
                proP[i * P[j]] = proP[i];
                break;
            }
            w[i * P[j]] = w[i] + 1;
            proP[i * P[j]] = proP[i] * P[j];
        }
    }

    // 得到 x 的所有因子的集合
    // 比O(nln(n))预处理快
    auto findDiv = [&](int x) {
        std::vector<int> res(1, 1);
        while (x > 1) {
            int d = v[x];
            x /= d;
            for (int i = res.size() - 1; i >= 0; i--)
                res.push_back(res[i] * d);
        }
        return res;
    };

    for (int i = 1; i < N; i++) {
        auto Div = findDiv(proP[i]);
        for (int d : Div) {
            L[i] = std::max(L[i], cnt[d]);
        }
        cnt[proP[i]] = i;
    }

    std::memset(cnt, 127, sizeof cnt);
    std::memset(R, 127, sizeof R);

    for (int i = N - 1; i; i--) {
        auto Div = findDiv(proP[i]);
        for (int d : Div) {
            R[i] = std::min(R[i], cnt[d]);
        }
        cnt[proP[i]] = i;
    }
    return 0;
}();

// 计数排序, 因为 w 很小, 所以计数排序比std::sort()快
void cpSort(std::vector<std::array<int, 3>> &a) {
    int M = 0; // 计数上界
    for (const auto &[w, x, y] : a)
        M = std::max(M, w);
    std::vector<std::vector<int>> Ton(M + 1);
    for (int i = 0; i < a.size(); i++) {
        Ton[a[i][0]].push_back(i);
    }
    std::vector<std::array<int, 3>> b;
    for (int x = 0; x <= M; x++) {
        for (auto i : Ton[x])
            b.push_back(a[i]);
    }
    a = b;
}

// 最小生成树函数
int mst(std::vector<std::array<int, 3>> &e, int l, int r) {
    DSU dsu(r - l + 1);
    cpSort(e);
    int ans = 0;
    for (auto [w, x, y] : e) {
        if (dsu.find(x - l) == dsu.find(y - l))
            continue;
        ans += w;
        dsu.Union(x - l, y - l);
    }
    return ans;
}

int edge(int x, int y) {
    return w[x] + w[y] - w[std::gcd(x, y)];
}

void solve() {
    int l, r;
    std::cin >> l >> r;

    int ans = std::accumulate(w + l, w + r + 1, 0);
    if (l == 1) {
        std::cout << ans << '\n';
        return;
    }

    std::vector<std::array<int, 3>> e; // Kruskal的边集合

    int primePos = 0;
    for (int x = l; x <= r; x++) {
        if (w[x] == 1)
            primePos = x;
    }

    if (primePos == 0) {
        // [l, r]区间里一个w[x]=1的数都没有, 直接暴力!
        for (int i = l; i < r; i++) {
            for (int j = i + 1; j <= r; j++) {
                e.push_back({edge(i, j), i, j});
            }
        }
        ans = mst(e, l, r);
        std::cout << ans << '\n';
        return;
    }

    for (int x = l; x <= r; x++) {
        // 向子集数连边不必客气, 边权直接是w[x]就行
        if (L[x] >= l)
            e.push_back({w[x], x, L[x]});
        if (R[x] <= r)
            e.push_back({w[x], x, R[x]});
        // 向不太熟悉的primePos连边客气点, 调用一下edge函数
        if (primePos != x)
            e.push_back({edge(x, primePos), x, primePos});
    }
    ans = mst(e, l, r);
    std::cout << ans << '\n';
}

E. Revenge on My Boss

题意 : 给一个n长度的三元组序列 [a_i,b_i, c_i] ,你需要重排这个序列,满足 \max \{ (\sum_{i=1}^m a_i + \sum_{i=m}^n b_i)*c_i \} 最小,求一个排列方案。

题解 :这是一个最小化最大值的问题,首先尝试二分,注意并不是所有最小化最大值的问题都能二分做,但是首先往这个方向尝试是可以的。

二分出一个结果 P ,那么 P 可行 等价于 存在一个重排方案使得 对于任意的 m 都满足  suma[1, m]+sumb[m, n] <= P/c[m] ,定义d[i] = a[i] - b[i] ,sumd[i] 是d的前缀和,B=sumb[1, n], 然后就可以转化为 sumd[m-1] <= P/c[m] + B - a[m] ,正如官方题解所言:不等式右边是与排列顺序无关的量,左边是与排列顺序有关量,定义不等式右边为 e[m],天哪这不就是那个经典的 luogu P1842 [USACO05NOV] 奶牛玩杂技 嘛,所以最贪心的方案就是按照 d[i]+e[i]从小到大排序即可, 但是这里的d[i]可能是负数,这里要单独处理,对于 d[i]<=0 的点,按照e[i] 从大到小 排序即可。

复杂度O(nlognlogV)

代码

using i64 = long long;

struct Node {
    int a, b, c, d, id;
    i64 e;
};

// sumb + sumd(m) + bm <= P/cm
// sumd(m) <= P/cm +sumb - bm
// sumd(m-1) <= P/cm +sumb - am

void solve() {
    int n;
    std::cin >> n;

    std::vector<Node> a(n);
    i64 sumb = 0;
    for (int i = 0; i < n; i++) {
        std::cin >> a[i].a >> a[i].b >> a[i].c;
        sumb += a[i].b;
        a[i].id = i + 1;
        a[i].d = a[i].a - a[i].b;
    }

    auto calc = [&](i64 x) {
        for (int i = 0; i < n; i++) {
            a[i].e = sumb - a[i].a + x / a[i].c;
        }
        std::sort(a.begin(), a.end(), [&](const Node &lhs, const Node &rhs) {
            if (lhs.d <= 0 and rhs.d <= 0)
                return lhs.e > rhs.e;
            if (lhs.d <= 0 or rhs.d <= 0)
                return lhs.d < rhs.d;
            return lhs.e + lhs.d < rhs.e + rhs.d;
        });
        i64 sumd = 0, Max = 0;
        for (int i = 0; i < n; i++) {
            sumd += a[i].d;
            Max = std::max(Max, 1ll * a[i].c * (sumb + sumd + a[i].b));
        }
        return Max <= x;
    };

    i64 l = 0, r = 1e18;
    // L : 存在一个排列使得 对于每个i都有 sumdi<=ei 是不可能的
    // R : 存在一个排列使得 对于每个i都有 sumdi<=ei 是可能的
    while (l + 1 < r) {
        auto mid = l + (r - l) / 2;
        if (calc(mid))
            r = mid;
        else
            l = mid;
    }
    calc(r);
    for (int i = 0; i < n; i++)
        std::cout << a[i].id << " \n"[i + 1 == n];
}

G. The Only Way to the Destination

题意 : 在N*M的网格图中放置K条横着的墙(我不知道为什么题目的图是画成竖着的)。最后判定剩余的空格子是否构成一棵树。 注意 :墙不会交叉,保证最后的空格子构成一个连通块。

1\le N,M\le 10^9, 1\le K \le 10^5

题解 : 笔者的题解和官方题解很不一样。

首先树有一个性质:n个节点伴随着n-1条边,所以我们只需要算出来最后剩余的空格子部分有几个点几条边即可。考虑对 y 方向离散化, 对 x 方向建立动态开点的线段树,从低到高不断求出最后的空格子图有几条边即可,复杂度O(Klogn)

代码

using i64 = long long;

// 动态开点线段树
struct Node {
    int val = 0, tag = 0;
    Node *l = nullptr;
    Node *r = nullptr;
};

void up(Node *p) {
    if (!p->l)
        p->val = p->r->val;
    else if (!p->r)
        p->val = p->l->val;
    else
        p->val = p->l->val + p->r->val;
}

void down(Node *p, int l, int r) {
    if (p->tag == 0)
        return;

    int mid = l + r >> 1;
    if (!p->l)
        p->l = new Node();
    if (!p->r)
        p->r = new Node();

    int b = p->tag;
    p->l->val += b * (mid - l + 1);
    p->r->val += b * (r - mid);
    p->l->tag += b;
    p->r->tag += b;

    p->tag = 0;
}

// 区间加 1
void modify(Node *&p, int l, int r, int tl, int tr) {
    if (!p)
        p = new Node();
    if (tl <= l and r <= tr) {
        p->val += r - l + 1;
        p->tag++;
        return;
    }
    down(p, l, r);
    int mid = l + r >> 1;
    if (tl <= mid)
        modify(p->l, l, mid, tl, tr);
    if (mid < tr)
        modify(p->r, mid + 1, r, tl, tr);
    up(p);
}

// 区间求和
int get(Node *p, int l, int r, int tl, int tr) {
    if (!p)
        return 0;
    if (tl <= l and r <= tr) {
        return p->val;
    }

    down(p, l, r);
    int mid = l + r >> 1, ans = 0;

    if (tl <= mid)
        ans = get(p->l, l, mid, tl, tr);
    if (mid < tr)
        ans += get(p->r, mid + 1, r, tl, tr);
    return ans;
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m, k;
    std::cin >> n >> m >> k;

    std::vector<std::array<int, 3>> wall(k);
    std::vector<int> b = {1, m};
    for (auto &[xl, xr, y] : wall) {
        std::cin >> xl >> xr >> y;
        b.push_back(y);
        if (y != 1)
            b.push_back(y - 1);
    }

    std::sort(b.begin(), b.end());
    b.erase(std::unique(b.begin(), b.end()), b.end());
    auto find = [&](int x) {
        return std::lower_bound(b.begin(), b.end(), x) - b.begin();
    }; // 离散化三件套, 把 y 给离散一下

    std::vector wallArr(b.size(), std::vector<std::pair<int, int>>());
    for (auto [xl, xr, y] : wall) {
        wallArr[find(y)].push_back({xl, xr});
    }

    i64 nodeCnt = 1ll * n * m, edgeCnt = 2ll * n * m - n - m;
    // 点数, 边数

    Node *rt = new Node(); // 开个线段树
    for (int y = 0; y < b.size(); y++) {

        if (!wallArr[y].empty()) {
            std::sort(wallArr[y].begin(), wallArr[y].end());
            for (int i = 0; i < wallArr[y].size(); i++) {
                auto [l, r] = wallArr[y][i];

                nodeCnt -= r - l + 1;
                edgeCnt += get(rt, 1, n, l, r) - (r - l + 2);
                edgeCnt -= 2 * (r - l + 1);

                // 墙在下底
                if (y == 0)
                    edgeCnt += r - l + 1;

                // 墙在上顶
                if (y + 1 == b.size())
                    edgeCnt += r - l + 1;

                // 俩墙左右贴着
                if (i and wallArr[y][i - 1].second + 1 == l)
                    edgeCnt++;

                // 墙贴着左侧
                if (i == 0 and l == 1)
                    edgeCnt++;

                // 墙贴着右侧
                if (i + 1 == wallArr[y].size() and r == n)
                    edgeCnt++;
            }
        }

        rt = new Node(); // 开个新的线段树
        for (auto [l, r] : wallArr[y]) {
            modify(rt, 1, n, l, r);
        }
    }

    std::cout << (edgeCnt + 1 == nodeCnt ? "YES" : "NO");
}

H. Energy Distribution

题意

给出一个 n×n 的矩阵 Wn×n, 在限制 (e[i] >= 0 \sum e_i = 1 ) 的基础下, 求

的最大值。

题解

已知 F(e1, e2, ... , en) 和限制, 求 F 的最大值,考虑拉格朗日乘数法,设 L(e1, e2, ... , en, λ) = F - λ(sum ei - 1) ,对 L 的每个变量求偏导,并且等于 0(注意消去λ),然后解出答案即可。因为题目还限制了 ei >= 0,所以我们枚举 ei = 0 的集合,然后针对其余的变量去高斯消元求解, 注意舍去负解。

代码

using ld = double;

constexpr ld eps = 1e-9;
int sgn(const ld &a) {
    if (a < -eps)
        return -1;
    return (a > eps);
}

std::string gauss(std::vector<std::vector<ld>> &a) { // 传入增广矩阵
    int n = a.size();
    int c = 0, r = 0;
    for (c = 0, r = 0; c < n; c++) { // c列r行,遍历列
        int tmp = r;
        for (int i = r; i < n; i++) // 寻找列主元
            if (sgn(a[i][c]))
                tmp = i;
        if (sgn(a[tmp][c]) == 0) // 当前列全为0
            continue;

        std::swap(a[tmp], a[r]); // 交换列主元

        for (int i = n; i >= c; i--) // 倒序处理
            a[r][i] /= a[r][c];

        for (int i = r + 1; i < n; i++)
            if (sgn(a[i][c]))
                for (int j = n; j >= c; j--)
                    a[i][j] -= a[r][j] * a[i][c];
        r++;
    }
    if (r < n) {
        for (int i = r; i < n; i++)
            if (sgn(a[i][n]))
                return "NoSolution";
        return "InfSolution";
    }

    // 解放在 a[i][n]  (0<= i < n)
    for (int i = n - 1; i >= 0; i--)
        for (int j = i + 1; j < n; j++)
            a[i][n] -= a[j][n] * a[i][j];
    return "OK";
}


int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n;
    std::cin >> n;

    std::vector w(n, std::vector<int>(n, 0));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            std::cin >> w[i][j];
        }
    }

    ld ans = 0;
    for (int S = 1; S < (1 << n); S++) {
        int m = __builtin_popcount(S);
        if (m <= 1)
            continue;
        std::vector a(m, std::vector<ld>(m + 1, 0));
        std::vector<int> b;
        for (int T = S; T; T -= T & -T) {
            b.push_back(std::__lg(T & -T));
        }
        for (int i = 0; i <= m; i++)
            a[0][i] = 1; // 第一行是 sum ei = 1
            
        for (int j = 1; j < m; j++)
            for (int k = 0; k < m; k++) {
                // 消去λ
                a[j][k] = w[b[j]][b[k]] - w[b[j - 1]][b[k]];
            }

        auto Solution = gauss(a);
        if (Solution == "OK") {
            ld res = 0;
            for (int i = 0; i < m; i++)
                if (sgn(a[i][m]) == -1)
                    res = -1e9; // 舍去负解
            for (int i = 0; i < m; i++) {
                for (int j = i + 1; j < m; j++) {
                    res += a[i][m] * a[j][m] * w[b[i]][b[j]];
                }
            }
            ans = std::max(ans, res);
        }
    }

    std::cout << std::fixed << std::setprecision(6) << ans;
}

J. Game on a Forest

题意 : 

1<= m < n <= 10^5

题解 : 我们手玩一下会发现,奇数个点的树的sg函数是1, 偶数个点的树的sg函数是2,注意0个点的树的sg函数是0。然后我们就可以暴力枚举删除点和边计算答案了,算是小小的树形dp。 复杂度O(n+m)

代码

struct DSU {
    std::vector<int> f;
    std::vector<int> size;

    DSU(int n) : f(n), size(n) {
        std::iota(f.begin(), f.end(), 0);
        std::fill(size.begin(), size.end(), 1);
    }

    int find(int x) {
        while (x != f[x])
            x = f[x] = f[f[x]];
        return x;
    }

    void Union(int x, int y) {
        if (find(x) == find(y))
            return;

        size[find(x)] += size[find(y)];
        f[find(y)] = find(x);
    }

    int blockSize(int x) {
        return size[find(x)];
    }
};

int sg(int x) {
    if (x == 0)
        return 0;
    return 2 - x % 2;
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

    DSU dsu(n + 1); // 开个并查集

    std::vector e(n + 1, std::vector<int>());
    for (int u, v, i = 0; i < m; i++) {
        std::cin >> u >> v;
        dsu.Union(u, v);
        e[u].push_back(v);
        e[v].push_back(u);
    }

    int SG = 0;
    for (int i = 1; i <= n; i++) {
        if (dsu.find(i) == i)
            SG ^= sg(dsu.blockSize(i));
    }

    int ans = 0;
    std::vector<int> size(n + 1, 0);
    std::function<void(int, int)> dfs = [&](int x, int fa) {
        size[x] = 1;
        int tmpSG = 0;
        for (int y : e[x]) {
            if (y == fa)
                continue;
            dfs(y, x);
            size[x] += size[y];
            tmpSG ^= sg(size[y]);
        }

        tmpSG ^= sg(dsu.blockSize(x) - size[x]);
        // 删点
        if ((SG ^ tmpSG) == 0)
            ans++;

        // 删边
        if (fa != 0 and (SG ^ sg(size[x]) ^ sg(dsu.blockSize(x) - size[x])) == 0) {
            ans++;
        }
    };

    for (int i = 1; i <= n; i++) {
        if (dsu.find(i) == i) {
            SG ^= sg(dsu.blockSize(i));
            dfs(i, 0);
            SG ^= sg(dsu.blockSize(i));
        }
    }

    std::cout << ans;
}

L. Palm Island

题意 : 给出两个长度为n的排列a和b,对a不断进行如下操作,需要你把a操作成b

操作1 : 把数组第一项取出,push_back到末尾。

操作2 : 把数组第二项取出,push_back到末尾。

1<= n <= 1000, 需要输出具体操作,且要求操作次数 <= n^2

题解 :

此题做法应该很多, 笔者是这样做的:

不断地通过操作2,使得a[1]右边接上a[1]这个元素右边应该接上的数字, 然后再用操作1把a[1]左边应该接上的元素旋转到第一位,然后重复上述操作n次即可。

比如样例2 :

[1, 2, 3, 4] 里根据b数组可知:1右边应该要接 3 的。

所以我们通过操作2把3旋转到第二位 → [1, 3, 4, 2]

根据b数组可知:1左边应该要接 2 的。

所以我们通过操作1把 2 旋转到第一位 → [2, 1, 3, 4]

然后接下来2后面要接1(已经接好了)

然后2左边需要是4。

所以我们通过操作1把 4 旋转到第一位 → [4, 2, 1, 3]

此时肯定已经循环同构了,最后我们把数组通过操作1旋转成b即可

代码

void solve() {
    int n;
    std::cin >> n;

    std::vector<int> a(n), b(n);
    for (int i = 0; i < n; i++)
        std::cin >> a[i], a[i]--;
    for (int i = 0; i < n; i++)
        std::cin >> b[i], b[i]--;

    std::vector<int> L(n);
    L[b[0]] = b.back();
    for (int i = 1; i < n; i++) {
        L[b[i]] = b[i - 1];
    }

    std::string res;
    for (int _ = 0; _ < n; _++) {
        int base = a[0];

        // 将L[a[0]]旋转到头
        auto pos = std::find(a.begin(), a.end(), L[base]);
        res.insert(res.end(), pos - a.begin(), '1');
        std::rotate(a.begin(), pos, a.end());

        // 将刚刚的a[0]旋转到1
        pos = std::find(a.begin(), a.end(), base);
        res.insert(res.end(), pos - a.begin() - 1, '2');
        std::rotate(a.begin() + 1, pos, a.end());
    }

    // 最后旋转到和答案一样
    auto pos = std::find(a.begin(), a.end(), b[0]);
    res.insert(res.end(), pos - a.begin(), '1');
    std::rotate(a.begin(), pos, a.end());

    std::cout << res << '\n';
}

M. Painter

题意 : 给n个操作

1,用字符在画布上画一个圆

2,用字符在画布上画一个矩形

3,输出画布矩形范围内的内容

只有1<= n <= 1000 ,其他输入数据均在[0, 10^9]内,操作3涉及的总面积<= 10^4

题解 : 

只需要每画一个位置,暴力扫描n个操作判定有没有在这个点画画即可。复杂度O(n*10^4)

(出题人说这里的10^4可以做到10^6,会有些难度)

代码

(这是笔者写的用到了类的继承和多态的第一个竞赛代码)

using pii = std::pair<int, int>;
#define x first
#define y second

pii operator-(pii a, pii b) {
    return {a.x - b.x, a.y - b.y};
}
i64 operator*(pii a, pii b) {
    return 1ll * a.x * b.x + 1ll * a.y * b.y;
}
i64 dis2(pii a, pii b) {
    auto p = a - b;
    return p * p;
}
bool Mid(int a, int b, int c) {
    return a <= b and b <= c;
}

struct Draw {
    std::string op = "";
    pii p1 = {0, 0};
    char col = '.';
    virtual bool Inside(pii p) = 0; // 纯虚函数
};
struct Circle : public Draw {
    int r = 0;
    bool Inside(pii p) { // 多态
        return dis2(p1, p) <= 1ll * r * r;
    }
};
struct Rectangle : public Draw {
    pii p2 = {0, 0};
    bool Inside(pii p) { // 多态
        return Mid(p1.x, p.x, p2.x) and Mid(p1.y, p.y, p2.y);
    }
};

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int qn;
    std::cin >> qn;

    std::vector<Draw *> a;
    for (int i = 0; i < qn; i++) {
        std::string op;
        std::cin >> op;
        if (op == "Circle") {
            Circle *p = new Circle();
            p->op = op;
            std::cin >> (p->p1.x) >> (p->p1.y);
            std::cin >> (p->r) >> (p->col);

            a.push_back(p);
        } else if (op == "Rectangle") {
            Rectangle *p = new Rectangle();
            p->op = op;
            std::cin >> (p->p1.x) >> (p->p1.y);
            std::cin >> (p->p2.x) >> (p->p2.y);
            std::cin >> (p->col);

            a.push_back(p);
        } else if (op == "Render") {
            pii p1, p2;
            std::cin >> p1.x >> p1.y;
            std::cin >> p2.x >> p2.y;

            std::vector res(p2.y - p1.y + 1, std::string(p2.x - p1.x + 1, '.'));

            for (auto p : a)
                for (int y = p1.y; y <= p2.y; y++)
                    for (int x = p1.x; x <= p2.x; x++)
                        if (p->Inside({x, y}))
                            res[y - p1.y][x - p1.x] = p->col;

            // 倒着输出
            for (int y = p2.y; y >= p1.y; y--) {
                std::cout << res[y - p1.y] << '\n';
            }
        }
    }
}

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值