2024CCPC 全国邀请赛(山东)暨山东省赛 题解 更新至 10 题

2024CCPC 全国邀请赛(山东)暨山东省赛 题解 更新至 10 题

Preface

这场打崩了,前有A题签到直接写不出来,后有M区间dp自己唐完了,很典的区间dp自己叭叭的半天,结果竟然写不出来,没有反应过来可以在区间dp的时候就可以算出来面积,总是想着要找出来点集然后暴力硬算,再加上算错了时间复杂度,最后直接give up了.只能说是非常的弱智了.

我会在代码一些有必要的地方加上注释,签到题可能一般就不会写了.

所有代码前面的火车头

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <unordered_map>
#include <iomanip>
#define endl '\n'
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep2(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
template<typename T>
void cc(vector<T> tem) { for (auto x : tem) cout << x << ' '; cout << endl; }
void cc(int a) { cout << a << endl; }
void cc(int a, int b) { cout << a << ' ' << b << endl; }
void cc(int a, int b, int c) { cout << a << ' ' << b << ' ' << c << endl; }
void fileRead() {
#ifdef LOCALL
    freopen("D:\\AADVISE\\cppvscode\\CODE\\in.txt", "r", stdin);
    freopen("D:\\AADVISE\\cppvscode\\CODE\\out.txt", "w", stdout);
#endif
}
void kuaidu() { ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); }
inline int max(int a, int b) { if (a < b) return b; return a; }
inline int min(int a, int b) { if (a < b) return a; return b; }
void cmax(int& a, const int b) { if (b > a) a = b; }
void cmin(int& a, const int b) { if (b < a) a = b; }
using PII = pair<int, int>;
using i128 = __int128;


//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------

Problem A. 打印机

很唐的一集啊,第一发没有注意中间可能爆\(long long\)导致WA,第二发在改的时候不知道怎么着把上界改成了\(1e9\)导致WA,但是没看出来,以为是大小还是不够的原因.

气的鼠鼠直接开启了\(int128\),一边骂着这狗题一边写,中间又因为不熟悉\(i128\)出现的各种问题,调了\(1h\)再过的.中间被队友带飞直接过了两道题.

实际上直接开\(long long\),上界调成\(2e9\)就好了.

思路就是二分时间,看每一台机器在这\(mid\)时间里最后造的数量有没有超过\(k\).

唉,一个一眼签到却调了\(1h\)的吃屎选手.


//--------------------------------------------------------------------------------
const int N = 1e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e18;
int n, m, T;
int t[N], L[N], W[N];
int k;
//--------------------------------------------------------------------------------
void out(i128 r) {
    string s = "";
    while (r) {
        s += (r % 10) + '0';
        r /= 10;
    }
    reverse(s.begin(), s.end());
    cout << s << endl;
}
int dfs(i128 mid) {
    i128 sum = 0;
    rep(i, 1, n) {
        i128 ll = i128(i128(W[i]) + i128(i128(t[i]) * i128(L[i])));
        // out(ll);
        sum += i128(i128(mid) / i128(ll) * i128(L[i]));
        if (sum >= k) return 1;

        i128 las = mid - ((mid / ll) * ll);
        if (las >= i128(t[i]) * i128(L[i])) sum += i128(L[i]);
        else {
            sum += i128(las / i128(t[i]));
        }

        if (sum >= k) return 1;
    }
    if (sum >= k) return 1;
    return 0;
}



signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n >> k;
        rep(i, 1, n) {
            cin >> t[i] >> L[i] >> W[i];
        }
        i128 l = 0, r = 1e19 + 2;
        // i128 rr = 1e20;
        // out(rr);
        // cc(r);
        // cout << (1ll << 62) << endl;
        while (l + 1 != r) {
            i128 mid = (l + r) / 2;
            // mid = 1e18;
            if (dfs(mid)) r = mid;
            else l = mid;
            // break;
        }
        // cout << r << endl;
        out(r);
    }
    return 0;
}
/*


*/

Problem C. 多彩的线段 2

这个题有点小搞笑了,上来第一眼直接口胡说把他转化成图上的问题,每一个线段看成一个点,然后有交集的线段就互相之间连一条边,然后相邻的点不能染一样的颜色,求所有的方案数.然后就屁都想不出来了,貌似后来搜了一下是图上的一个有点典的问题,复杂度不小.

只能说唐了,思考了之后无果,然后队友说好像是可以直接模拟做的,瞬间给我茅塞顿开.只能说签到题开的小丑了.

所以我们只需要这样按照左端点排个序,从左到右开始遍历,然后对于枚举当前的线段,找前面有几个跟它接触的,有几个就 \(k\) 减去几,这就是它自己对于答案的贡献数字,最后乘起来就好了.

稍微注意的就是为了快点查询有几个接触的,可以用个堆(里面按照右端点排序),这样最后复杂度多了一个 \(log\)

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 998244353;
const int INF = 1e16;
int n, m, T;
PII A[N];
//--------------------------------------------------------------------------------
struct node {
    int y;
    bool operator<(const node& q1) const {
        return q1.y < y;
    }
};


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        int k;
        cin >> n >> k;
        rep(i, 1, n) {
            int a, b; cin >> a >> b;
            A[i] = { a,b };
        }
        int tem = k;
        sort(A + 1, A + n + 1, [&](PII a, PII b) {
            return a.first < b.first;
            });
        priority_queue<node> F;
        int ans = 1;
        rep(i, 1, n) {
            while (!F.empty() and F.top().y < A[i].first) {
                tem++;
                F.pop();
            }
            ans *= tem--;
            ans %= mod;
            F.push({ A[i].second });
        }
        
        cc(ans);
    }
    return 0;
}
/*


*/

Problem D. 王国英雄

首先能够想到的就是我们一定是先能买多少买多少,再能卖多少卖多少,称这个为 \(one\) 一个周期,然后我们一直进行这个周期.
先说一个基本的式子:
当前拥有的钱是 \(m\),最多能买的面粉就是 \(x=m/p (向下取整)\),那么 \(one\) 之后我们的钱是 \(m+(q-p)*x\),耗费的时间是 \(t=(ax+b+cx+d)\)

但是如果我们只是这样做,时间复杂度并不会允许.因为可能 \(a,b,c,d,x\)都会很小,时间复杂度会直接卡成 \(O(t)\) 级别的,

队友张神提出了一个很好的优化,就是其实我们还能够进一步求出来能够买 \(x+1\) 需要的时间, 即设购买 \(l\) 轮之后我们可以买 \(x+1\) 个面粉了,那么就有 \(m+l*(q-p)*x>=(x+1)*p\) ,即 $l >= (p(x+1)-m)/((q-p)x) $ ,向上取整就是 \(l\) 的取值.

这样时间复杂度因为 \(x\) 每次枚举都会 $ +1$ (请联想\(1+2+...+n=t\)这个式子),所以会变成 \(O(\sqrt[2]t)\).

那么还有要考虑的就是最后一段,当我们之后不能一直买到 \(x+1\) 的面粉了,剩下的一段时间,我们直接二分处理(懒得想式子)就好了.


//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        int p, a, b, q, c, d, t;
        cin >> p >> a >> b >> q >> c >> d >> m >> t;

        if (m < p) {
            cout << m << endl;
            continue;
        }

        int ci, t1, ll;
        while (1) {
            //ci是上文提到的x
            ci = m / p;
            //t1是买一次的时间
            t1 = (a + c) * ci + b + d;
            //ll是刚才说的那个能够买x+1个面粉的轮数,分子上多了一部分分母-1是为了向上取整,例如a/b,如果想要向上取整,就写(a+b-1)/b
            ll = (p * (ci + 1) - m + (q - p) * ci - 1) / ((q - p) * ci);
            // cmin(ll, t / (t1));
            //t是我们目前还剩余的时间,如果不够就break
            if (ll * t1 > t) break;
            t -= ll * t1;
            m += ll * (q - p) * ci;
        }
        //跳出来之后,记得算一下我们能够买几次x个面粉
        m += t / t1 * (q - p) * ci;
        t -= t / t1 * t1;
        //之后二分买面粉的个数,如果二分的个数在剩余的时间能够买完再卖完就l=mid,最后l就是我们最后买的面粉.
        int l = 0, r = t + 1;
        while (l + 1 != r) {
            int mid = l + r >> 1;
            if (a * mid + b + c * mid + d <= t) l = mid;
            else r = mid;
        }
        m += (q - p) * l;
        cout << m << endl;


    }
    return 0;
}
/*


*/

Problem E. 传感器

非常有意思的一道题,赛时被M单防了,没有看着题,之后补题想着思路是能不能直接把传感器挂到线段树上,然后再做操作,但又觉得时间复杂度不太允许,便作罢.结果发现还真是这样做,但是要加上一些小优化.

首先先开一个线段树,线段树上的节点维护的信息有 \(sum (代表红球的个数之和)\),还有一个 $vector A \(里面存传感器编号,当前节点有这个编号就代表编号覆盖了这个区间,且不会向下\)(\(就是说如果\)[1,4]$区间的节点有编号\(1\),那么\([1,2]\)区间和\([3,4]\)区间就不会有\()\) 另外再设一个 \(val[i]\) 代表第\(i\)个传感器里有多少个红球

这样空间是不会爆的,节点里的\(vector\)上最多会有 \(mlogm\) 个编号,但是在更新的时候想着每次少一个,如果编号都要更新\(val\)的话,最后时间复杂度会变成 \(O(nmlogm)\) .

但是实际上我们只关心 \(sum\) 是不是1而已,设当前区间长度\(len\),我们只当 $sum==1 $ 或者 \(sum==0\)的时候再更新就好了. 如果\(sum==1\)\(val\) 就减\(len-1\),这样复杂度里的 \(n\) 就会变成常数.

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int val[N];
int ans;
//--------------------------------------------------------------------------------
//namespace or struct:
//线段树板子,里面改了add函数,又加了一个dfs函数用来维护节点上的传感器
namespace seg {
#define xl x+x
#define xr x+x+1
    const int N = 5e5 + 10; const int LIM = N * (2.1);
    struct node {
        int sum = 1;
        // vector<int> A;
    };
    node F[LIM];
    vector<int> A[LIM];
    node operator+(const node& q1, const node& q2) {
        node q;
        // q.A.clear();
        q.sum = q1.sum + q2.sum;
        return q;
    }
    void apply(int x, int k) {
        F[x].sum += k;
    }

    void init(int x, int l, int r) {
        A[x].clear();
        if (l == r) {
            F[x] = node();
            //记得清空A,这里WA了两发
            A[x].clear();
            return;
        }
        int mid = l + r >> 1;
        init(xl, l, mid), init(xr, mid + 1, r);
        F[x] = F[xl] + F[xr];
    }
    void add(int x, int l, int r, int l1, int r1, int k) {
        if (l1 > r1) return;
        if (l1 <= l and r <= r1) {
            apply(x, k);
            if (F[x].sum == 0) {
                //遍历当前节点的传感器,val[id]都减1,因为区间长度只能是1此时
                for (auto& id : A[x]) {
                    val[id] -= 1;
                    if (val[id] == 0) ans -= id * id;
                    if (val[id] == 1) ans += id * id;

                }
            }
            return;
        }
        int mid = l + r >> 1;
        if (r1 <= mid) add(xl, l, mid, l1, r1, k);
        else if (mid < l1) add(xr, mid + 1, r, l1, r1, k);
        else add(xl, l, mid, l1, mid, k), add(xr, mid + 1, r, mid + 1, r1, k);
        F[x] = F[xl] + F[xr];

        if (F[x].sum == 1) {
            for (auto& id : A[x]) {
                val[id] -= r - l + 1 - 1;
                if (val[id] == 1) ans += id * id;
                if (val[id] == 0) ans -= id * id;
            }
        }
        else if (F[x].sum == 0) {
            for (auto& id : A[x]) {
                val[id] -= 1;
                if (val[id] == 1) ans += id * id;
                if (val[id] == 0) ans -= id * id;
            }
        }

    }
    node qry(int x, int l, int r, int l1, int r1) {
        if (l1 > r1) return node();
        if (l1 <= l and r <= r1) return F[x];
        int mid = l + r >> 1;
        if (r1 <= mid) return qry(xl, l, mid, l1, r1);
        else if (mid < l1) return qry(xr, mid + 1, r, l1, r1);
        else { return qry(xl, l, mid, l1, mid) + qry(xr, mid + 1, r, mid + 1, r1); }
    }
    //实现上述说的节点里的vector
    void dfs(int x, int l, int r, int l1, int r1, int& id) {
        if (l1 <= l and r <= r1) {
            A[x].push_back(id);
            return;
        }
        int mid = (l + r) >> 1;
        if (r1 <= mid) dfs(xl, l, mid, l1, r1, id);
        else if (mid < l1) dfs(xr, mid + 1, r, l1, r1, id);
        else {
            dfs(xl, l, mid, l1, mid, id);
            dfs(xr, mid + 1, r, mid + 1, r1, id);
        }
    }
#undef xl
#undef xr
}
//-----------------------

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n >> m;
        seg::init(1, 1, n);
        rep(i, 1, m) {
            int l, r; cin >> l >> r;
            //小球的下标是从0开始的!!!
            l += 1, r += 1;
            val[i] = r - l + 1;
            if (val[i] == 1) ans += i * i;
            seg::dfs(1, 1, n, l, r, i);
        }
        cout << ans << " ";
        rep(i, 1, n) {
            int a; cin >> a;
            a += ans; a %= n; a += 1;
            // cc(a);
            seg::add(1, 1, n, a, a, -1);
            cout << ans << " ";
        }
        cout << endl;

    }
    return 0;
}
/*


*/

Problem F. 分割序列

首先对于这种\(i*s[i]\)的求和,(\(s[i]\)代表局部内的和,共\(k\)部分),我们可以直接直观的把他转化成是后缀和.每一部分的划分后的贡献相当于是这一部分的起点 \(l\) 一直到 \(n\) 的求和.这里可以画图理解一下,会更加形象.

所以我们只需要求出来前 \(k-1\) 大的后缀和就好了,划分 \(k\) 个部分,相当于是切了 \(k-1\) 刀对这个数列.

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N], suf[N];
//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n;

        rep(i, 0, n + 1) suf[i] = 0;

        rep(i, 1, n) {
            cin >> A[i];
        }
        priority_queue<int> F;
        int sum = 0;
        rep2(i, n, 2) {
            sum += A[i];
            suf[i] = suf[i + 1] + A[i];
            F.push({ suf[i] });
        }
        sum += A[1];
        cout << sum << " ";
        while (!F.empty()) {
            auto val = F.top(); F.pop();
            sum += val;
            cout << sum << " ";
        }
        cout << endl;

    }
    return 0;
}
/*


*/

Problem H. 阻止城堡

原谅我是一个\(SB\),我竟然一开始想的只需要在十字路口的时候就放就可以了,码了半天结果被样例\(hark\)了.\(ok\),重新整理思路...

这个题写的就十分\(恶心\)了,能够半模半觉的感觉化成二分图或者往网络流那边靠,但不知道该怎么写出来,看了题解才知道怎么维护,学到了学到了.

我们可以将里面需要放障碍物的情况分成两类:一类是x轴上相邻的,一类是y轴上相邻的.但是有一种情况就是有时候一个障碍物可以同时阻挡以上两类.但不能简单的直接只要是一个十字路口就放一个障碍物,我们可以考虑以下:

\[O \ \ \ \ \ O \]

\[O \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ O \]

\[O \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ O \]

\[O \ \ \ \ \ O \]

我们发现实际只需要两个障碍物就可以了,放了一个障碍物之后会影响别的地方,这很二分图的感觉.

平常的二分图里面将点分成左右两部分,然后跑匈牙利.对于这道题我们可以将左右相邻的点看做左边的点.上下相邻的点看做右边的点,然后如果有十字路口那么就给这两个新点连一条边,建完图之后跑匈牙利.


//--------------------------------------------------------------------------------
const int N = 5e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
bool ff[N];
//--------------------------------------------------------------------------------
//二分图板子
namespace KM {
    const int N = 5e2 + 10;
    int n, m;
    vector<int> A[N];//n
    int co[N];//m
    bool vis[N];//m
    void init(int n_, int m_) {
        n = n_, m = m_;
        for (int i = 0; i <= max(n, m); i++) {
            A[i].clear();
            co[i] = -1;
        }
    }
    void add(int x, int y) { A[x].push_back(y); }
    bool dfs(int x) {
        for (auto y : A[x]) {
            if (vis[y]) continue; vis[y] = 1;
            if (co[y] == -1 or dfs(co[y])) { co[y] = x; return 1; }
        }
        return 0;
    }
    int work() {
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= m; j++) vis[j] = 0;
            if (dfs(i)) cnt++;
        }
        return cnt;
    }
}
struct node {
    int id;
    int q1;
    int q2;
};

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        unordered_map<int, vector<PII>> X, Y;
        cin >> n;

        rep(i, 0, n) ff[i] = 0;

        rep(i, 1, n) {
            int a, b; cin >> a >> b;
            //X轴上的点,1代表是人,0代表是障碍物
            X[a].push_back({ b,1 });
            Y[b].push_back({ a,1 });
        }
        cin >> m;
        rep(i, 1, m) {
            int a, b; cin >> a >> b;
            X[a].push_back({ b,0 });
            Y[b].push_back({ a,0 });
        }
        vector<node> XX, YY;
        //XX是代表二分图中左边的点,YY是代表右边的点
        bool flag = 1;
        for (auto [x, A] : X) {
            sort(A.begin(), A.end(), [&](PII a, PII b) {
                return a.first < b.first;
                });
            PII las = { -INF,0 };
            for (auto [id, fl] : A) {
                //如果las.secong==0 or fl==0:代表同轴上已经有放的障碍物了,就可以直接不管了,不需要放到XX或者YY中
                if (las.first == -INF || (fl == 0 or las.second == 0)) {
                    las.first = id, las.second = fl;
                    continue;
                }
                //判断有没有中间挨着的点,有就是说明没有方案
                if (id == las.first + 1) flag = 0;
                XX.push_back({ x,las.first,id });
                las.first = id, las.second = fl;
            }
        }
        for (auto [y, A] : Y) {
            sort(A.begin(), A.end(), [&](PII a, PII b) {
                return a.first < b.first;
                });
            PII las = { -INF,0 };
            for (auto [id, fl] : A) {
                if (las.first == -INF || (fl == 0 or las.second == 0)) {
                    las.first = id, las.second = fl;
                    continue;
                }
                if (id == las.first + 1) flag = 0;
                YY.push_back({ y,las.first,id });
                las.first = id, las.second = fl;
            }
        }
        // for (auto [a, b, c] : XX) {
        //     cc(a, b, c);
        // }
        // for (auto [a, b, c] : YY) {
        //     cc(a, b, c);
        // }
        if (flag == 0) {
            cout << -1 << endl;
            continue;
        }

        KM::init(XX.size(), YY.size());
        int n1 = XX.size(), m1 = YY.size();
        rep(i, 0, n1 - 1) rep(j, 0, m1 - 1) {
            auto [x, y1, y2] = XX[i];
            auto [y, x1, x2] = YY[j];
            if (x1 < x and x < x2 and y1 < y and y < y2) {
                //满足十字路口的就加边
                KM::add(i, j);
                // cc(x, y);
            }
        }
        KM::work();
        using KM::co;
        // rep(i, 0, m1 - 1) cc(i, co[i]);
        // cc(n1);
        vector<PII> ans;
        rep(i, 0, m1 - 1) {
            //代表在这个十字路口放一个障碍物
            if (co[i] != -1) {
                ff[co[i]] = 1;
                ans.push_back({ XX[co[i]].id,YY[i].id });
            }
        }
        rep(i, 0, n1 - 1) {
            if (ff[i]) continue;
            ans.push_back({ XX[i].id,(XX[i].q1 + XX[i].q2) / 2 });
        }
        rep(i, 0, m1 - 1) {
            if (co[i] != -1) continue;
            ans.push_back({ (YY[i].q1 + YY[i].q2) / 2, YY[i].id });
        }
        cout << ans.size() << endl;
        for (auto [a, b] : ans) {
            cout << a << " " << b << endl;
        }
    }
    return 0;
}
/*


*/

Problem I. 左移

一眼纯纯签到,只需要把字符串复制一遍,然后 \(for\) 循环 \(i\) 扫一遍 $ s[i] $和 \(s[i+len]\) (即字符串的头和尾)一不一样就好了.

//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        string s; cin >> s;
        if (s.size() == 1 || (s[0] == s[s.size() - 1])) {
            cout << 0 << endl;
            continue;
        }
        int len = s.size();
        s = s + s;
        bool fl = 0;
        rep(i, 0, len - 1) {
            if (s[i] == s[i + len - 1]) {
                cout << i << endl;
                fl = 1;
                break;
            }
        }
        if (!fl) cout << -1 << endl;

    }
    return 0;
}

Problem J. 多彩的生成树

一眼最小生成树,但是需要一些详细的分类讨论.

先从小到大排序边权,然后合并.
并查集里面有两个元素,一个是\(fa\),一个是\(fl\).//分别代表并查集内部的\(fa\)和当前并查集内部有没有合并过.

如果合并的\(fa\)一样,但是\(fl=0\)(代表没有合并过),那就是联通块的内部合并.
如果\(fl==1\),就可以直接\(return\)了.

以下都是\(fa\)不一样的情况,如果\(x\)\(y\)\(fl\)都是\(1\),那么两个联通块之间联一条边就好;
如果有一个是\(1\),那么\(ans\)就加\(没有内部联通的联通块大小*当前的边权\).
如果都是0,那么就是加\((联通块大小的和-1)*当前的边权\).


int ans = 0;
int val;
namespace DSU {
    const int N = 1e3 + 10;
    int A[N];
    struct Info {
        int fa;
        int siz;
        int fl;
    };
    Info dsu[N];
    void init(int n) {
        //TO DO 记得初始化
        rep(i, 0, n) {
            dsu[i].fa = i, dsu[i].siz = 1;
            dsu[i].fl = 0;
        }
    }
    int find(int x) { if (x == dsu[x].fa) return x; return dsu[x].fa = find(dsu[x].fa); }
    void merge(int x, int y) {
        x = find(x), y = find(y);
        if (x == y and dsu[x].fl) return;
        if (x == y) {
            ans += (A[x] - 1) * val;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[x].fl == 1 and dsu[y].fl == 1) {
            ans += val;
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[x].fl == 0 and dsu[y].fl == 0) {
            ans += (A[x] + A[y] - 1) * val;
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[x].fl == 0) {
            ans += (A[x] * val);
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[y].fl == 0) {
            ans += A[y] * val;
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }

    }
    bool same(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return 1; return 0;
    }
    int size(int x) { return dsu[find(x)].siz; }
}
using DSU::dsu;
using DSU::A;
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
struct node {
    int x;
    int y;
    int val;
};
vector<node> ed;
//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n;
        DSU::init(n);
        ans = 0;
        ed.clear();

        rep(i, 1, n) {
            cin >> A[i];
        }
        rep(i, 1, n) {
            rep(j, 1, n) {
                int a; cin >> a;
                ed.push_back({ i,j, a });
            }
        }
        sort(ed.begin(), ed.end(), [&](node& q1, node& q2) {
            return q1.val < q2.val;
            });

        for (auto [x, y, val_] : ed) {
            // cc(x, y, val_);
            val = val_;
            DSU::merge(x, y);
        }
        cout << ans << endl;
    }
    return 0;
}
/*


*/

Problem K. 矩阵

这个就没什么特别好讲解的了,一共 \(2n\) 个数字,并且只有一个子矩阵是四个角都互不一样的.

小小构造题,做法有很多.这里直接说一种做法,貌似也是题解的做法.

直接让前 \(n-2\) 行从上到下为1,2,3,...,然后最后倒数两行从左往右是n-1,n,...一直到剩下最后两列,目前填了 \(2n-4\)个了,然后把剩下四个没有填的数字放进去就好了.


//--------------------------------------------------------------------------------
const int N = 5e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N][N];
//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;

        int cnt = 0;
        rep(i, 1, n - 2) {
            cnt++;
            rep(j, 1, n) A[i][j] = cnt;
        }

        rep(j, 1, n - 2) {
            cnt++;
            rep(i, n - 1, n) {
                A[i][j] = cnt;
            }
        }

        A[n][n] = ++cnt, A[n - 1][n] = ++cnt;
        A[n - 1][n - 1] = ++cnt, A[n][n - 1] = ++cnt;

        cout << "Yes" << endl;
        rep(i, 1, n) {
            rep(j, 1, n) {
                cout << A[i][j] << " ";
            }
            cout << endl;
        }

    }
    return 0;
}
/*


*/

Problem L. 路径的交

前置知识:每一次修改一条边权 动态维护树的直径

这个题没有补,实在是补不动了.但是大体思路是差不多的.就是最终的路径如果能被选择,他的两段一定都是要大于等于\(k_i\)的.所以我们如果在原本的树上从叶子节点开始都去掉\(k_i\)个点之后,剩余的新树我们跑一下直径就好了.

以上是\(k\)固定的情况,那\(k\)不固定的时候,我们可以离线处理答案,使\(k\)从小到大,那么树就是从外层开始一层一层被删去的,(删边权可以使得边权为\(0\)),删去后再修改一条边权再求树的直径,转化成板子题了.为了这个题专门去补了其前置知识.

不贴代码有点难受,贴一个动态维护树直径的板子吧...


//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int w;
struct node {
    int x;
    int y;
    int val;
};
vector<node> ed;
vector<PII> A[N];
int dis[N], L[N], R[N], dep[N];
int tot;
int O[N];

//--------------------------------------------------------------------------------
//namespace :
namespace seg {
#define xl x+x
#define xr x+x+1
    //TODO 调整N的大小
    const int N = 2e5 + 10; const int LIM = N * (2.7);
    struct node {
        int ans = 0;
        int mmax = 0;
        int mmin = 0;
        int rm = 0;
        int lm = 0;
        int lan = 0;
    };
    node F[LIM];
    //TODO up函数
    node operator+(const node& q1, const node& q2) {
        node q;
        q.lan = 0;
        q.ans = max({ q1.ans,q2.ans,q1.mmax + q2.lm,q1.rm + q2.mmax });
        q.mmax = max(q1.mmax, q2.mmax);
        q.mmin = min(q1.mmin, q2.mmin);
        q.lm = max({ q1.lm,q2.lm,q2.mmax - 2 * q1.mmin });
        q.rm = max({ q1.rm,q2.rm,q1.mmax - 2 * q2.mmin });
        return q;
    }
    //TODO apply函数
    void apply(int x, int k) {
        F[x].lm -= k;
        F[x].rm -= k;
        F[x].mmax += k;
        F[x].mmin += k;
        F[x].lan += k;
    }

    void init(int x, int l, int r) {
        if (l == r) { F[x] = node(); return; }
        int mid = l + r >> 1;
        init(xl, l, mid), init(xr, mid + 1, r);
        F[x] = F[xl] + F[xr];
    }
    void down(int x) { if (!F[x].lan) return; apply(xl, F[x].lan), apply(xr, F[x].lan); }
    void add(int x, int l, int r, int l1, int r1, int k) {
        if (l1 > r1) return;
        if (l != r) down(x); F[x].lan = 0;
        if (l1 <= l and r <= r1) { apply(x, k); return; }
        int mid = l + r >> 1;
        if (r1 <= mid) add(xl, l, mid, l1, r1, k);
        else if (mid < l1) add(xr, mid + 1, r, l1, r1, k);
        else add(xl, l, mid, l1, mid, k), add(xr, mid + 1, r, mid + 1, r1, k);
        F[x] = F[xl] + F[xr];
    }
    node qry(int x, int l, int r, int l1, int r1) {
        if (l1 > r1) return node();
        if (l != r) down(x); F[x].lan = 0;
        if (l1 <= l and r <= r1)  return F[x];
        int mid = l + r >> 1;
        if (r1 <= mid) return qry(xl, l, mid, l1, r1);
        else if (mid < l1) return qry(xr, mid + 1, r, l1, r1);
        else { return qry(xl, l, mid, l1, mid) + qry(xr, mid + 1, r, mid + 1, r1); }
    }
#undef xl
#undef xr
}
//------------------------------------

void dfs(int x, int pa) {
    dep[x] = dep[pa] + 1;
    L[x] = ++tot;
    O[tot] = x;
    for (auto [y, val] : A[x]) {
        if (y == pa) continue;
        dis[y] = dis[x] + val;
        dfs(y, x);
        O[++tot] = x;
    }
    R[x] = tot;
}

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        int q; cin >> n >> q >> w;
        rep(i, 1, n - 1) {
            int a, b, c; cin >> a >> b >> c;
            ed.push_back({ a,b,c });
            A[a].push_back({ b,c });
            A[b].push_back({ a,c });
        }
        dfs(1, 0);
        seg::init(1, 1, tot);
        rep(i, 1, tot) {
            seg::add(1, 1, tot, i, i, dis[O[i]]);
        }
        int las = 0;
        rep(i, 1, q) {
            int d, e;
            cin >> d >> e;
            int dd = (d + las) % (n - 1);
            int ee = (e + las) % w;
            int t;
            if (dep[ed[dd].x] > dep[ed[dd].y]) t = ed[dd].x;
            else t = ed[dd].y;
            // if (i == 3) cc(dd, t, ee - ed[dd].val);
            seg::add(1, 1, tot, L[t], R[t], ee - ed[dd].val);
            las = seg::qry(1, 1, tot, 1, tot).ans;
            cout << las << endl;
            ed[dd].val = ee;
        }
    }
    return 0;
}
/*


*/

Problem M. 回文多边形

非常吃屎的一集,看到题手玩一下,发现很像区间\(dp\),但是又觉得不太能\(ok\),鉴定是区间\(dp\)做少了导致的,回去恶补区间地痞.中间想的时候也想到了这个题里面的好几个\(trick\),后面看题解的时候一眼就会了,想死.

假设一个数组是(点代表别的数字)

\[3 \ \ 3 \ .\ .\ .\ \ 3 \ . \ 3 \ . \ . \ 3 \ \ . \ . \ 3 \]

我们如果要从两端往中间做选择,逐渐选到最后,形成一个回文的序.
\(trick1:\)我们不会在不选择第一个\(3\)和最后一个\(3\)的情况去选择里面别的\(3\),如果有这种情况,我们肯定还要在把最外层的\(3\)一对也都选上才对,这样的面积才是最大的.

\(trick2:\)我们\(dp\)的下标\(l,r\)定义成选择了\(l,r\)两个点的最终值,有便于方程转移和计算.假设当前的两段是\(l,r\),找到的下一个两段是\(l_1,r_1\),那么我们完全可以计算出这之间的面积.把它拆成两个三角形,利用向量计算就好了.这样在转移的时候,我们可以用以上方式算出来,而不是暴力出点集再算面积

\(trick3:\)我们在\(trick1\)的基础上,要注意,我们可以选择最左边的\(3\)和中间的一个\(3\)作为\(l_1和r_1\),这样算出来的面积也是有可能是最大值.

还有时间复杂度的问题,递归的复杂度看不出来可以看转化成递推看,\(l,r\)的枚举是\(O(n^2)\),还有端点和中间的匹配还会有一个\(n\),所以最后是\(O(n^3)\)

\(hh\)最傻的地方是我后来按照这种想法想冲一发,但是dfs写错了导致时间复杂度搞错了最后直接开摆.

所以\(dp\)转移主要围绕上面几点来,剩下的详看代码:

//离散化板子
struct LISAN {
    vector<int> F;
    void init(int A[], int n) {
        vector<int>().swap(F);
        rep(i, 1, n) F.push_back(A[i]);
        sort(F.begin(), F.end());
        F.erase(unique(F.begin(), F.end()), F.end());
    }
    void init(vector<int> A) {
        for (auto x : A) F.push_back(x);
        sort(F.begin(), F.end());
        F.erase(unique(F.begin(), F.end()), F.end());
    }
    //找到第一个大于等于val
    int findda(int val) { int x = lower_bound(F.begin(), F.end(), val) - F.begin() + 1; return x; }
    //找到最后一个小于等于val
    int findxi(int val) { int x = upper_bound(F.begin(), F.end(), val) - F.begin(); return x; }
    void change(int A[], int n) {
        rep(i, 1, n) A[i] = findda(A[i]);
    }
};

struct POINT {
    int x;
    int y;
};
//计算面积
int cal(POINT a, POINT b, POINT c) {
    int tem = 0;
    tem = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
    if (tem < 0) tem *= -1;
    return tem;
}
//--------------------------------------------------------------------------------
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int dp[N][N];
int A[N];
//zuo[i][j]数组代表从i的下标往左,最近的权值=j的下标,you数组同理
int zuo[N][N], you[N][N];
LISAN ds;
POINT pos[N];
//--------------------------------------------------------------------------------


int dfs(int l, int r) {
    // cc(12312321);
    if (dp[l][r] != -1) return dp[l][r];
    if (l >= r) return dp[l][r] = 0;
    if (A[l] != A[r]) return dp[l][r] = 0;
    if (l + 1 == r) return dp[l][r] = 0;
    // cc(l, r);
    int tem = 0;
    //枚举i,找到距离r-1左边最近的一样的值当我们新的端点
    rep(i, l + 1, r) {
        int j = zuo[r - 1][A[i]];
        if (i > j) continue;
        cmax(tem, dfs(i, j) + cal(pos[l], pos[r], pos[i]) + cal(pos[i], pos[j], pos[r]));
    }
    //与上面同理,枚举j,找到距离l+1右边最近的一样的值当新的端点
    //这样的遍历方式使得我们不会有上述的选择中间的3而没有选择最两端的3
    rep2(j, r - 1, l) {
        int i = you[l + 1][A[j]];
        if (i > j) continue;
        cmax(tem, dfs(i, j) + cal(pos[l], pos[r], pos[i]) + cal(pos[i], pos[j], pos[r]));
    }
    // cc(tem);
    dp[l][r] = tem;
    return dp[l][r];
}

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {

        cin >> n;


        rep(i, 1, n + n) rep(j, 1, n + n) {
            dp[i][j] = -1;
            zuo[i][j] = 0;
            you[i][j] = 0;

        }

        rep(i, 1, n) {
            cin >> A[i];
            A[n + i] = A[i];
        }
        rep(i, 1, n) {
            int a, b; cin >> a >> b;
            pos[i] = { a,b };
            pos[i + n] = { a,b };
        }

        int mmax = 0;
        //离散化,因为点数不多,但是权值很大.
        ds.init(A, n + n);
        ds.change(A, n + n);
        rep(i, 1, n) cmax(mmax, A[i]);
        // cc(mmax);
        //预处理zuo数组
        rep(i, 1, n + n) {
            rep(j, 1, mmax) {
                zuo[i][j] = zuo[i - 1][j];
                if (A[i] == j) zuo[i][j] = i;
            }
        }
        //预处理you数组
        rep2(i, n + n, 1) {
            rep(j, 1, mmax) {
                you[i][j] = you[i + 1][j];
                if (A[i] == j) you[i][j] = i;
            }
        }

        // rep(i, 1, n + n) cout << A[i] << endl;
        //计算ans
        int ans = 0;
        // ans = dfs(1, 3);
        rep(i, 1, n + n) {
            rep(j, i + 1, n + n) {
                if (j - i + 1 > n) break;
                cmax(ans, dfs(i, j));
            }
        }
        cout << ans << endl;
    }
    return 0;
}
/*


*/

PostScript

这场打得稀巴烂,前期吃屎后期坐牢,评价是要狠狠的训.

posted @ 2024-11-15 01:05  AdviseDY  阅读(1)  评论(0编辑  收藏  举报