【多校训练】2022杭电多校4补题

官方数据和标程:2022杭电多校资料包

难度评价

中后段题目跨度很大的一场。
7题一百名,5题快两百名,4题四百名。至少补到6题,金牌补到8题以上。
D、F、G、K签到,A、B、C中等,E困难,H、I、J大难。


A. Link with Bracket Sequence II

DP

待补


B. Link with Running

图论

显然直接跑最短路即可得到答案1,最短路图是一个DAG,在DAG上跑最长路(拓扑排序递推DP)即可求得答案2。
然而本题边权可能为0,这意味着在dijkstra中,即使一个点 u u u 已被访问过,如果对边 ( v → u , w = 0 ) (v \rightarrow u,w=0) (vu,w=0) d [ v ] + 0 = = d [ u ] d[v]+0==d[u] d[v]+0==d[u],则它也应该出现在最短路图中。由此可以发现最短路图可能存在0环,即它不是DAG。

在这里插入图片描述
如图,对应的图中存在0环,且该图直接等价于其最短路图。因为不是DAG,所以不能DP求最长路,如果尝试大力跑SPFA最长路会喜提TLE,dijkstra跑最长路会直接WA。因此需要先缩点成DAG,再跑拓扑排序。

注:题目保证答案存在且不为无穷大,说明如果在1到n的路径上有0环,则第二权值 p p p 也一定全为0;如果该0环不在1到n的路径上,则对答案无影响。

#include <bits/stdc++.h>
using namespace std;
#define int int64_t

constexpr int MAXN = 1e5 + 5;
using tpl = tuple<int, int, int>;
using PII = pair<int, int>;
vector<tpl> G[MAXN], T[MAXN], E[MAXN];
int n, m, d[MAXN], low[MAXN], num[MAXN], c[MAXN], in[MAXN], dfn, cnt;
bool vis[MAXN], inst[MAXN];
stack<int> st;

void dijkstra(int s = 1)
{
    fill(vis + 1, vis + n + 1, 0);
    fill(d + 1, d + n + 1, 1e18);
    d[s] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.emplace(0, s);
    while (!q.empty())
    {
        auto [_, v] = q.top();
        q.pop();
        if (vis[v])
            continue;
        vis[v] = 1;

        for (auto [u, w, p] : G[v])
            if (d[v] + w < d[u])
                d[u] = d[v] + w, q.emplace(d[u], u);
    }
    for (int v = 1; v <= n; v++)
        for (auto [u, w, p] : G[v])
            if (d[v] + w == d[u])
                T[v].emplace_back(u, w, p);
}

void tarjan(int v)
{
    low[v] = num[v] = ++dfn;
    st.push(v), inst[v] = 1;
    for (auto [u, w, p] : T[v])
    {
        if (!num[u])
        {
            tarjan(u);
            low[v] = min(low[v], low[u]);
        }
        else if (inst[u])
            low[v] = min(low[v], num[u]);
    }
    if (num[v] == low[v])
    {
        ++cnt;
        int z;
        do
        {
            z = st.top(), st.pop(), inst[z] = 0;
            c[z] = cnt;
        } while (v != z);
    }
}

void connect()
{
    for (int v = 1; v <= n; v++)
        for (auto [u, w, p] : T[v])
            if (c[v] != c[u])
                E[c[v]].emplace_back(c[u], w, p), ++in[c[u]];
}

int topoSort()
{
    vector<int> dp(cnt + 1);
    queue<int> q;
    for (int i = 1; i <= cnt; i++)
        if (in[i] == 0)
            q.push(i);
    while (!q.empty())
    {
        int v = q.front();
        q.pop();
        for (auto [u, w, p] : E[v])
        {
            dp[u] = max(dp[u], dp[v] + p);
            if (--in[u] == 0)
                q.push(u);
        }
    }
    return dp[c[n]];
}

void clear()
{
    dfn = cnt = 0;
    fill(num + 1, num + n + 1, 0);
    for (int i = 1; i <= n; i++)
        G[i].clear(), T[i].clear(), E[i].clear();
}

int32_t main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        cin >> n >> m;
        while (m--)
        {
            int x, y, w, p;
            cin >> x >> y >> w >> p;
            G[x].emplace_back(y, w, p);
        }
        dijkstra();
        for (int i = 1; i <= n; i++)
            if (!num[i])
                tarjan(i);
        connect();
        cout << d[n] << " " << topoSort() << endl;
        clear();
    }

    return 0;
}

C. Magic

图论

比较明显的差分约束,容易想到用前缀和 a [ i ] a[i] a[i] 表示前 i i i 个位置放的魔法原料之和,则不等式组为
{ a [ m i n ( n , i + k − 1 ) ] − a [ m a x ( 0 , i − k ) ] ≥ p i ( 1 ) a [ R j ] − a [ L j − 1 ] ≤ B j ( 2 ) a [ i ] − a [ i − 1 ] ≥ 0 ( 3 ) \begin{cases} a[min(n,i+k-1)]-a[max(0,i-k)] \geq p_i & (1) \\ a[R_j]-a[L_j-1] \leq B_j & (2)\\ a[i]-a[i-1] \geq 0 & (3)\\ \end{cases} a[min(n,i+k1)]a[max(0,ik)]pia[Rj]a[Lj1]Bja[i]a[i1]0(1)(2)(3)
其中 ( 1 ) (1) (1) 代表位置 i i i 的魔力值要大于 p i p_i pi ( 2 ) (2) (2) 代表第 j j j 个约束条件所规定的区间 [ L j , R j ] [L_j,R_j] [Lj,Rj] 內的原料之和不能多于 B j B_j Bj ( 3 ) (3) (3) 是容易遗漏的条件,代表每个位置的原料数必须非负。
因为要求最小值,建出图后跑spfa最长路即可,有正环无解。

#include <bits/stdc++.h>
using namespace std;

constexpr int MAXN = 1e4 + 5;
vector<pair<int, int>> G[MAXN];
int n, k, q, cnt[MAXN], d[MAXN];
bool inq[MAXN];

bool spfa(int s = 0)
{
    fill(d, d + n + 1, -1e9);
    fill(inq, inq + n + 1, 0);
    fill(cnt, cnt + n + 1, 0);
    queue<int> q;
    q.push(s);
    d[s] = 0, cnt[s]++, inq[s] = 1;
    while (!q.empty())
    {
        int v = q.front();
        q.pop();
        inq[v] = 0;
        for (auto [u, w] : G[v])
        {
            if (d[u] < d[v] + w)
            {
                d[u] = d[v] + w;
                if (!inq[u])
                {
                    q.push(u);
                    if (++cnt[u] > n)
                        return 1;
                    inq[u] = 1;
                }
            }
        }
    }
    return 0;
}

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        cin >> n >> k;
        for (int i = 1, p; i <= n; i++)
        {
            cin >> p;
            G[max(0, i - k)].emplace_back(min(n, i + k - 1), p); // a[min(n, i+k-1)] - a[max(0, i-k)] >= p
        }
        cin >> q;
        while (q--)
        {
            int l, r, b;
            cin >> l >> r >> b;
            G[r].emplace_back(l - 1, -b); // a[r] - a[l-1] <= b
        }
        for (int i = 1; i <= n; i++)
        {
            G[i - 1].emplace_back(i, 0); // a[i] - a[i-1] >= 0
            G[0].emplace_back(i, 0);
        }
        cout << (spfa() ? -1 : d[n]) << "\n";
        for (int i = 0; i <= n; i++)
            G[i].clear();
    }

    return 0;
}

D. Link with Equilateral Triangle

构造

诈骗签到题。连 n = 3 n=3 n=3 都画不出来,直接猜无解。也许可以打表看小数据,但是复杂度也挺高的。证明可以参照官方题解。

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int t;
    cin >> t;
    while (t--) cout << "No\n";
    return 0;
}

E. Link with Level Editor II

数学数据结构双指针

注意到图很小,考虑用矩阵维护点对可达性(就是邻接矩阵),则矩阵乘即为方案数。如下图所示,

在这里插入图片描述
在这里插入图片描述
由于题目要求方案数不多于 k k k,对一个可行区间 [ l , r ] [l,r] [l,r],显然 r r r 增大时方案数非降, l l l 增大时方案数非升,比较明显的双指针性质,于是枚举区间可以在 O ( N ) O(N) O(N) 时间复杂度內完成。
则问题在于如何快速支持查询。因为是区间矩阵乘,自然想到线段树,则复杂度包括建树和每次区间查询,为 O ( N M 3 + N M 3 l o g N ) O(NM^3+NM^3logN) O(NM3+NM3logN),因为还有多测,太慢而无法通过。官方题解使用了对顶栈的做法,这可以避免线段树每次都重新查询区间矩阵乘的弊端,实现了和矩阵求逆一样的效果,在 O ( N M 3 ) O(NM^3) O(NM3) 时间内通过本题。然而线段树做法仍然有优化空间。
瓶颈来自于每次暴力矩阵乘,考虑到我们只关心 1 1 1 n n n 的方案数,仅使用一个单位矩阵 [ 1 , 0 , 0 , ⋯   , 0 ] [1,0,0,\cdots,0] [1,0,0,,0] 右乘上线段树区间即可优化掉一个 M M M 的复杂度,总时间复杂度为 O ( N M 3 + N M 2 l o g N ) O(NM^3+NM^2logN) O(NM3+NM2logN)

#include <bits/stdc++.h>
using namespace std;

constexpr int MAXN = 5005, M = 21;
int n, m;
int64_t k, mat[MAXN << 2][M][M], tmp1[M][M], tmp2[M], f[M];

void mul(int64_t res[M][M], int64_t a[M][M], int64_t b[M][M])
{
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= m; j++)
            tmp1[i][j] = 0;
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= m; j++)
            for (int k = 1; k <= m; k++)
                tmp1[i][j] += a[i][k] * b[k][j];
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= m; j++)
            res[i][j] = tmp1[i][j];
}

void mul(int64_t res[M], int64_t a[M], int64_t b[M][M])
{
    for (int i = 1; i <= m; i++)
        tmp2[i] = 0;
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= m; j++)
            tmp2[i] += a[j] * b[j][i];
    for (int i = 1; i <= m; i++)
        res[i] = tmp2[i];
}

void build(int l, int r, int rt)
{
    if (l == r)
    {
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= m; j++)
                mat[rt][i][j] = (i != j ? 0 : 1);
        int e, v, u;
        cin >> e;
        while (e--)
            cin >> v >> u, mat[rt][v][u] = 1;
        return;
    }
    int mid = (l + r) >> 1;
    build(l, mid, rt << 1);
    build(mid + 1, r, rt << 1 | 1);
    mul(mat[rt], mat[rt << 1], mat[rt << 1 | 1]);
}

void query(int L, int R, int l, int r, int rt)
{
    if (L <= l && r <= R)
    {
        mul(f, f, mat[rt]);
        return;
    }
    int mid = (l + r) >> 1;
    if (L <= mid)
        query(L, R, l, mid, rt << 1);
    if (R > mid)
        query(L, R, mid + 1, r, rt << 1 | 1);
}

bool check(int l, int r)
{
    for (int i = 1; i <= m; i++)
        f[i] = 0;
    f[1] = 1;
    query(l, r, 1, n, 1);
    return f[m] <= k;
}

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        cin >> n >> m >> k;
        build(1, n, 1);
        int l = 1, r = 0, ans = 0;
        while (l <= n)
        {
            while (r < n && check(l, r + 1))
                ++r;
            ans = max(ans, r - l + 1);
            ++l;
        }
        cout << ans << "\n";
    }

    return 0;
}

F. BIT Subway

数学模拟

实际价格直接模拟算,以为的价格计算起来有点阅读理解,但是注意到票可以任意拆开,意味着可以分段享受所有优惠而不亏损,因此求出票价和 s s s,答案是关于 s s s 的分段函数(注意区分花费与票价和,例如票价和为225时花费才200)
a n s = { s , 0 ≤ s ≤ 100 100 + ( s − 100 ) ∗ 0.5 , 100 < s ≤ 225 200 + ( s − 225 ) ∗ 0.8 , s > 225 ans= \begin{cases} \qquad \qquad s, & & 0 \leq s \leq 100 \\ 100+(s-100)*0.5, & & 100 < s \leq 225 \\ 200+(s-225)*0.8, & & s > 225 \end{cases} ans= s,100+(s100)0.5,200+(s225)0.8,0s100100<s225s>225

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cout << fixed << setprecision(3);
    int t;
    cin >> t;
    while (t--)
    {
        int n;
        cin >> n;
        vector<int> a(n);
        for (auto &x : a)
            cin >> x;
        double ans1 = accumulate(a.begin(), a.end(), 0), ans2 = 0;
        if (ans1 > 100 && ans1 <= 225)
            ans1 = 100 + 0.8 * (ans1 - 100);
        else if (ans1 > 225)
            ans1 = 200 + 0.5 * (ans1 - 225);
        for (auto x : a)
        {
            if (ans2 < 100)
                ans2 += x;
            else if (ans2 >= 100 && ans2 < 200)
                ans2 += x * 0.8;
            else
                ans2 += x * 0.5;
        }
        cout << ans1 << " " << ans2 << endl;
    }

    return 0;
}

G. Climb Stairs

贪心模拟数据结构

显然每次跳过一些怪物后,必须一直往下打败它们,再往上跳,对于多个可选跳点 r 1 , r 2 , ⋯   , r i r_1,r_2,\cdots,r_i r1,r2,,ri,选择 r 1 r_1 r1 总不劣(因为依然可以跳到已有跳点,还可能增加新的跳点)。于是问题变为每次寻找一个最小的 r r r
设当前在 c u r cur cur,已访问过的最远点为 t o p top top,暴力枚举端点 t o p < c u r + i ≤ m i n ( c u r + k , n ) top<cur+i \leq min(cur+k,n) top<cur+imin(cur+k,n),并检查其是否是可能的跳点,如图所示。
在这里插入图片描述
注意到每次检查失败就会进行区间加,容易想到线段树,但是贡献来自单点,区间操作很尴尬。稍作转换,只要左侧最小值大于等于右侧最小值,所有不等式即满足,区间最小值和区间加可以用线段树维护。
实际上不需要线段树,因为这里的区间加是全局加,线性方法详见代码。

#include <bits/stdc++.h>
using namespace std;

constexpr int MAXN = 1e5 + 5;
int64_t a[MAXN], sum[MAXN];

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        int n, k, cur = 0, top = 0;
        int64_t atk;
        cin >> n >> atk >> k;
        for (int i = 1; i <= n; i++)
            cin >> a[i], sum[i] = sum[i - 1] + a[i];
        while (top != n)
        {
            bool ok = 0;
            int64_t mn = 1e18;
            for (int i = top - cur + 1; i <= k && cur + i <= n; i++)
            {
                mn = min(mn, atk - a[cur + i]);
                if (mn >= 0)
                {
                    atk += sum[cur + i] - sum[top];
                    top = cur + i, ++cur;
                    ok = 1;
                    break;
                }
                mn += a[cur + i + 1];
            }
            if (!ok)
                break;
        }
        cout << (top == n ? "YES\n" : "NO\n");
    }

    return 0;
}

H. Fight and upgrade

计算几何数据结构

过难


I. Fall with Full Star

DP贪心

过难


J. Fall with Intersection

博弈论计算几何

过难


K. Link is as bear

数学

举例子直接猜测是线性基板题,即在 n n n 个数中选出若干个数,使得它们的异或和最大。详细证明见官方题解。

#include <bits/stdc++.h>
using namespace std;

int64_t p[64];

void insert(int64_t x)
{
    for (int i = 60; i >= 0; i--)
    {
        if (!(x >> i))
            continue;
        if (!p[i])
        {
            p[i] = x;
            break;
        }
        x ^= p[i];
    }
}

int64_t getMax()
{
    int64_t ans = 0;
    for (int i = 60; i >= 0; i--)
        ans = max(ans, ans ^ p[i]);
    return ans;
}

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        memset(p, 0, sizeof(p));
        int n;
        cin >> n;
        int64_t x;
        for (int i = 1; i <= n; i++)
            cin >> x, insert(x);
        cout << getMax() << "\n";
    }

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NoobDream_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值