2022年杭电多校第四场补题记录

A Link with Bracket Sequence II

题意:给定长度为 n n n 的括号序列,有 m m m 种括号,该括号序列中有些位置为空。问利用这 m m m 种括号填充括号序列中的空位置,将该序列变成合法括号序列的方案数。 n ≤ 500 n \leq 500 n500 m ≤ 1 × 1 0 9 m \leq 1\times 10^9 m1×109

解法: n ≤ 500 n \leq 500 n500 第一反应是区间 dp。但是朴素的 f i , j f_{i,j} fi,j 表示区间 [ i , j ] [i,j] [i,j] 变成一个合法括号序列方案数会导致重复:例如 ()()()在合并的时候会出现 ()()|()()|()()这两种分法,但是其实是一个方案。其原因在于一次合并进来了多个同层的括号序列。其改进方法为:

f i , j f_{i,j} fi,j 表示区间 [ i , j ] [i,j] [i,j] 变为合法的括号序列方案数, g i , j g_{i,j} gi,j 表示区间 [ i , j ] [i,j] [i,j] 由合法区间 [ i + 1 , j − 1 ] [i+1,j-1] [i+1,j1] 再直接套上外层的合法括号对组成的方案数。则转移方法为:
g i , j = { f i + 1 , j − 1 , a i  和  a j  可以完全确定一对合法的括号 m f i + 1 , j − 1 , a i = a j = 0 f i , j = ∑ k = i + 1 j − 1 f i , k g k + 1 , j g_{i,j}= \begin{cases} f_{i+1,j-1},a_i\ \text{和}\ a_j\ \text{可以完全确定一对合法的括号}\\ mf_{i+1,j-1},a_i=a_j=0 \end{cases} \\ f_{i,j}=\sum_{k=i+1}^{j-1}f_{i,k}g_{k+1,j} gi,j={fi+1,j1,ai  aj 可以完全确定一对合法的括号mfi+1,j1,ai=aj=0fi,j=k=i+1j1fi,kgk+1,j
这样转移之后,上面提到的重复问题就会得到解决,因为每次都是合并进来一个同层的括号序列,而不是多个同层的括号序列。总复杂度 O ( n 3 ) \mathcal O(n^3) O(n3)

#include <bits/stdc++.h>
using namespace std;
const long long mod = 1000000007;
const int N = 500;
long long f[N + 5][N + 5], g[N + 5][N + 5];
int a[N + 5];
int main()
{
    int t, n, m;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n;i++)
            scanf("%d", &a[i]);
        for (int i = 1; i <= n;i++)
            f[i][i - 1] = g[i][i - 1] = 1;
        for (int len = 2; len <= n;len += 2)
            for (int i = 1; i + len - 1 <= n;i++)
            {
                int j = i + len - 1;
                if(a[i] == 0 && a[j] == 0)
                    f[i][j] = g[i][j] = f[i + 1][j - 1] * m % mod;
                else if((a[i] > 0 && a[j] == 0) || (a[i] == 0 && a[j] < 0) || (a[i] > 0 && a[j] < 0 && a[i] + a[j] == 0))
                    f[i][j] = g[i][j] = f[i + 1][j - 1];
                else
                    f[i][j] = g[i][j] = 0;
                for (int k = i + 1; k < j; k += 2)
                    f[i][j] = (f[i][j] + f[i][k] * g[k + 1][j] % mod) % mod;
            }
        printf("%lld\n", f[1][n]);
        for (int i = 1; i <= n; i++)
            for (int j = 0; j <= n; j++)
                f[i][j] = g[i][j] = 0;
    }
    return 0;
}

B Link with Running

题意: n n n 个点 m m m 条边的带权有向图,图上每个点有非负距离和非负价值,问从 1 1 1 号点走到 n n n 号点的最近距离,以及在最近距离下能获得的最大价值。 n ≤ 1 × 1 0 5 n\leq 1\times 10^5 n1×105 m ≤ 3 × 1 0 5 m \leq 3\times 10^5 m3×105

解法:不可以直接多重费用的最短路因为有 0 0 0 边。同理最短路图上跑 Dijkstra 也是错误的,错误样例如下:

3 3
1 3 0 2
1 2 0 1
2 3 0 2

正解为先用 Dijkstra 计算出最短路图,但是由于可能出现 0 0 0 环因而这个图不一定是有向无环图 DAG,需要通过缩点使图变成 DAG,然后再利用拓扑排序进行 DAG 上 dp。总时间复杂度为 O ( n + n log ⁡ n ) \mathcal O(n+n\log n) O(n+nlogn)

#include <bits/stdc++.h>
using namespace std;
const long long inf = 0x3f3f3f3f3f3f3f3fll;
const int N = 200000, M = 300000;
struct line
{
    int from;
    int to;
    long long v;
    long long gain;
    int next;
};
struct line que[M + 5];
struct edge
{
    int from;
    int to;
    long long gain;
    edge(int _from, int _to, long long _gain)
    {
        from = _from;
        to = _to;
        gain = _gain;
    }
};
int cnt, headers[N + 5];
void add(int from, int to, long long v, long long gain)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].v = v;
    que[cnt].gain = gain;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
vector<edge> graph[N + 5], newgraph[N + 5];
struct node
{
    int id;
    long long dis;
    node(int _id, long long _dis)
    {
        id = _id;
        dis = _dis;
    }
    bool operator<(const node &b)const
    {
        return dis > b.dis;
    }
};
int n, m;
long long dis[N + 5];
bool vis[N + 5];
void dijkstra(int s, int t)
{
    for (int i = 1; i <= n;i++)
    {
        dis[i] = inf;
        vis[i] = 0;
    }
    dis[s] = 0;
    priority_queue<node> q;
    q.emplace(s, 0);
    while (!q.empty())
    {
        auto tp = q.top();
        q.pop();
        if (vis[tp.id])
            continue;
        vis[tp.id] = 1;
        for (int i = headers[tp.id]; i; i = que[i].next)
            if(dis[que[i].to] > dis[tp.id] + que[i].v)
            {
                dis[que[i].to] = dis[tp.id] + que[i].v;
                q.emplace(que[i].to, dis[que[i].to]);
            }
    }
}
int dfn[N + 5], low[N + 5], ind, col[N + 5], tot;
stack<int> s;
void tarjan(int u)
{
    dfn[u] = low[u] = ++ind;
    s.push(u);
    vis[u] = 1;
    for (auto i : graph[u])
        if(!dfn[i.to])
        {
            tarjan(i.to);
            low[u] = min(low[u], low[i.to]);
        }
        else if (vis[i.to])
            low[u] = min(dfn[i.to], low[u]);
    if (low[u] == dfn[u])
    {
        tot++;
        while (s.top() != u)
        {
            col[s.top()] = tot;
            vis[s.top()] = 0;
            s.pop();
        }
        col[u] = tot;
        vis[u] = 0;
        s.pop();
    }
}
int deg[N + 5];
long long f[N + 5];
void topo(int s)
{
    queue<int> q;
    for (int i = 1; i <= tot;i++)
        if(!deg[i])
            q.push(i);
    while(!q.empty())
    {
        int tp = q.front();
        q.pop();
        for (auto i : newgraph[tp])
        {
            deg[i.to]--;
            f[i.to] = max(f[i.to], f[tp] + i.gain);
            if(!deg[i.to])
                q.push(i.to);
        }
    }
}
int main()
{
    int t;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        for (int i = 1, u, v; i <= m; i++)
        {
            long long dis, gain;
            scanf("%d%d%lld%lld", &u, &v, &dis, &gain);
            add(u, v, dis, gain);
        }
        dijkstra(1, n);
        for (int i = 1; i <= n; i++)
            for (int j = headers[i]; j; j = que[j].next)
                if(dis[que[j].to] == dis[i] + que[j].v)
                    graph[i].emplace_back(i, que[j].to, que[j].gain);
        for (int i = 1; i <= n;i++)
            vis[i] = 0;
        printf("%lld ", dis[n]);
        for (int i = 1; i <= n;i++)
            if(!dfn[i])
                tarjan(i);
        for (int i = 1; i <= n;i++)
            for (auto j : graph[i])
                if (col[i] != col[j.to])
                    newgraph[col[i]].emplace_back(col[i], col[j.to], j.gain);
        for (int i = 1; i <= tot;i++)
            for (auto j : newgraph[i])
                deg[j.to]++;
        topo(col[1]);
        printf("%lld\n", f[col[n]]);
        for (int i = 1; i <= n; i++)
        {
            graph[i].clear();
            newgraph[i].clear();
            headers[i] = dfn[i] = low[i] = col[i] = deg[i] = f[i] = 0;
        }
        while(!s.empty())
            s.pop();
        cnt = ind = tot = 0;
    }
    return 0;
}

C Magic

题意: n n n 个城市,第 i i i 城市需要 p i p_i pi 的魔法值。给定范围 k k k,魔法值可以通过在城市里面堆放魔法原料来获得,对于第 i i i 个城市投放了一个魔法原料,可以让 [ max ⁡ ( 1 , i − k + 1 ) , min ⁡ ( n , i + k − 1 ) ] [\max(1,i-k+1),\min(n,i+k-1)] [max(1,ik+1),min(n,i+k1)] 的所有城市都获得一个魔法值。此外有 q q q 条限制 ( l i , r i , b i ) (l_i,r_i,b_i) (li,ri,bi),表示区间 [ l i , r i ] [l_i,r_i] [li,ri] 的城市中堆放的魔法原料总和不超过 b i b_i bi。问是否有合法方案,若有输出最少需要堆放多少魔法原料。 n , q ≤ 1 × 1 0 4 n,q \leq 1\times 10^4 n,q1×104

解法:若考虑魔法原料堆放的前缀和 s i s_i si,则原问题转化为差分约束问题:

  1. 对于每个城市 i i i 的魔法值需求 p i p_i pi,需要满足 s min ⁡ ( n , i + k − 1 ) − s max ⁡ ( 1 , i − k ) ≥ p i s_{\min(n,i+k-1)}-s_{\max(1,i-k)} \geq p_i smin(n,i+k1)smax(1,ik)pi——只有 [ max ⁡ ( 1 , i − k + 1 ) , min ⁡ ( n , i + k − 1 ) ] [\max(1,i-k+1),\min(n,i+k-1)] [max(1,ik+1),min(n,i+k1)] 的范围内堆放魔法原料才能让 i i i 城市获得魔法值。
  2. 对于 q q q 条限制,需要满足 s r i − s l i − 1 ≤ b i s_{r_i}-s_{l_i-1} \leq b_i srisli1bi
  3. 每个城市堆放的魔法原料个数非负, s i − s i − 1 ≥ 0 s_i-s_{i-1} \geq 0 sisi10

因而直接使用差分约束的 spfa 算法找负环即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 10000, M = 50000;
struct line
{
    int from;
    int to;
    int v;
    int next;
};
struct line que[M + 5];
int cnt, headers[N + 5], dis[N + 5], times[N + 5], n;
bool vis[N + 5];
void add(int from, int to, int v)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].v = v;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
bool spfa(int s)
{
    for (int i = 1; i <= n;i++)
        dis[i] = -inf;
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    while(!q.empty())
    {
        int tp = q.front();
        q.pop();
        if (times[tp] >= n)
            return 0;
        times[tp]++;
        vis[tp] = 0;
        for (int i = headers[tp];i;i=que[i].next)
            if(dis[que[i].to]<dis[tp]+que[i].v)
            {
                dis[que[i].to] = dis[tp] + que[i].v;
                if(!vis[que[i].to])
                {
                    vis[que[i].to] = 1;
                    q.push(que[i].to);
                }
            }
    }
    return 1;
}
int main()
{
    int t, k, q;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &k);
        for (int i = 1, x; i <= n;i++)
        {
            add(i - 1, i, 0);
            scanf("%d", &x);
            int l = max(1, i - k + 1), r = min(n, i + k - 1);
            add(l - 1, r, x);
        }
        scanf("%d", &q);
        for (int i = 1, l, r, b; i <= q;i++)
        {
            scanf("%d%d%d", &l, &r, &b);
            add(r, l - 1, -b);
        }
        dis[0] = 0;
        if(!spfa(0))
            printf("-1\n");
        else
            printf("%d\n", dis[n]);
        for (int i = 0; i <= n;i++)
            vis[i] = headers[i] = times[i] = 0;
        cnt = 0;
    }
    return 0;
}

E Link with Level Editor II

题意: n n n 层图,每层 m m m 个节点,每层图给定。在第 i i i 层每次可以选择走一步或者停留,然后上升到 i + 1 i+1 i+1 层,所处点编号不变。问一个最长的连续子段 [ l , r ] [l,r] [l,r],使得从第 l l l 层的 1 1 1 号节点出发,到 m m m 号节点的方案数不超过 k k k n ≤ 5 × 1 0 3 n \leq 5\times 10^3 n5×103 m ≤ 20 m \leq 20 m20 k ≤ 1 × 1 0 9 k \leq 1\times 10^9 k1×109

解法:由于 m m m 非常小可以考虑使用 m × m m \times m m×m 的转移矩阵来辅助转移。首先预处理出每一层的转移矩阵 M i M_i Mi,考虑区间 [ l , r ] [l,r] [l,r] 1 1 1 m m m 的方案数,为 [ 1 0 0 ⋯ 0 ] ∏ i = l r M i \displaystyle \begin{bmatrix}1&0&0 &\cdots &0\end{bmatrix}\prod_{i=l}^r M_i [1000]i=lrMi 的第 m m m 项。

l l l 从小到大的变化时,容易发现 r r r 是单调递增的,因而可以使用双指针。枚举 l l l 移动 r r r,维护初始的列向量,每次等效于向已有的矩阵乘以 M r M_r Mr,单次乘法复杂度为 O ( m 2 ) \mathcal O(m^2) O(m2)。之所以不维护初始的矩阵,是因为利用结合律可以加快朴素维护 m × m m\times m m×m 矩阵的 O ( m 3 ) \mathcal O(m^3) O(m3) 的矩阵乘法。每次 l l l 向右移动一位时,使用线段树计算出 [ l + 1 , r ] [l+1,r] [l+1,r] 的乘积。这样的复杂度为 O ( n m 2 log ⁡ n ) \mathcal O(nm^2 \log n) O(nm2logn)

#include <bits/stdc++.h>
using namespace std;
long long lim;
struct matrix
{
    int n, m;
    vector<vector<long long>> p;
    matrix() = default;
    matrix(int _n, int _m)
    {
        n = _n;
        m = _m;
        this->p.resize(_n);
        for (auto &i : p)
            i.resize(_m);
    }
    void set(int _n, int _m)
    {
        n = _n;
        m = _m;
        p.resize(_n);
        for (auto &i : p)
            i.resize(_m);
        if (n == m)
            for (int i = 0; i < n;i++)
                p[i][i] = 1;
    }
    matrix operator*(matrix x)
    {
        int a = this->n, b = x.m, c = this->m;
        matrix ans(a, b);
        for (int i = 0; i < a;i++)
            for (int j = 0; j < b;j++)
                for (int k = 0; k < c;k++)
                {
                    ans.p[i][j] += this->p[i][k] * x.p[k][j];
                    if (ans.p[i][j] > lim)
                    {
                        ans.p[i][j] = lim + 1;
                        break;
                    }
                }
        return ans;
    }
    bool check()
    {
        return p[0][m - 1] <= lim;
    }
};
class segment_tree
{
    vector<matrix> t;
    int n, m;
    matrix res;
    void query(int place, int left, int right, int start, int end)
    {
        if (start <= left && right <= end)
        {
            res = res * t[place];
            return;
        }
        int mid = (left + right) >> 1;
        matrix ans(m, m);
        if (start <= mid)
            query(place << 1, left, mid, start, end);
        if (end > mid)
            query(place << 1 | 1, mid + 1, right, start, end);
        return;
    };

public:
    segment_tree(int n, int m, vector<matrix> &que)
    {
        this->n = n;
        this->m = m;
        t.resize(4 * n + 5);
        function<void(int, int, int)> build = [&](int place, int left, int right)
        {
            if (left == right)
            {
                t[place] = que[left];
                return;
            }
            int mid = (left + right) >> 1;
            build(place << 1, left, mid);
            build(place << 1 | 1, mid + 1, right);
            t[place] = t[place << 1] * t[place << 1 | 1];
        };
        build(1, 1, n);
    }
    matrix query(int left, int right)
    {
        res.set(1, m);
        for (int i = 0; i < m; i++)
            res.p[0][i] = 0;
        res.p[0][0] = 1;
        query(1, 1, n, left, right);
        return res;
    }
};
int main()
{
    int caset, n, m;
    scanf("%d", &caset);
    while(caset--)
    {
        scanf("%d%d%lld", &n, &m, &lim);
        vector<matrix> layer(n + 1);
        for (int i = 1, l, x, y; i <= n;i++)
        {
            layer[i].set(m, m);
            scanf("%d", &l);
            if (l)
                while (l--)
                {
                    scanf("%d%d", &x, &y);
                    layer[i].p[x - 1][y - 1] = 1;
                }
        }
        segment_tree t(n, m, layer);
        int ans = 1;
        matrix base = layer[1];
        for (int l = 1, r = 1; l <= n; l++, base = t.query(l, r))
        {
            while (r < n)
            {
                matrix temp = base * layer[r + 1];
                if(temp.check())
                {
                    r++;
                    base = temp;
                }
                else
                    break;
            }
            ans = max(ans, r - l + 1);
            if (r == n)
                break;
    }
    printf("%d\n", ans);
    }
    return 0;
}

还可以使用对顶栈。枚举 r r r 然后 l l l 向右推进,维护一个栈,第 i i i 项存储 ∏ j = i r M j \displaystyle \prod_{j=i}^r M_j j=irMj,栈顶为编号最小的元素。对于 r r r 一次右移,只需要给栈中所有元素乘以 M r + 1 M_{r+1} Mr+1 即可。这样的复杂度为 O ( n m 3 ) \mathcal O(nm^3) O(nm3)

G Climb Stairs

题意:给定 n + 1 n+1 n+1 层楼 { 0 , 1 , 2 , ⋯   , n } \{0,1,2,\cdots,n\} {0,1,2,,n},从一楼开始第 i i i 层有个怪物血量为 a i a_i ai,只有当攻击值大于等于 a i a_i ai 时才能击杀怪物,同时击杀血量为 a i a_i ai 的怪物可以使攻击值增加 a i a_i ai。初始攻击值为 a 0 a_0 a0,每次可以向上跳跃 1 , 2 , ⋯   , k 1,2,\cdots, k 1,2,,k 层或者向下走一楼,前提是能击杀这个怪物,问能否恰好到访每层一次,击杀每一层的怪物然后走到 n n n 楼。 1 ≤ k ≤ n ≤ 1 × 1 0 5 1 \leq k \leq n \leq 1\times 10^5 1kn1×105

解法:显然路径形如 0 , ( x 1 , x 1 − 1 , ⋯   , 1 ) , ( x 2 , x 2 − 1 , ⋯   , x 1 + 1 ) , ( x 3 , x 3 − 1 , ⋯   , x 2 + 1 ) , ⋯   , ( n − 1 , n − 2 , ⋯   , x k + 1 ) , n 0,(x_1,x_1-1,\cdots,1),(x_2,x_2-1,\cdots,x_1+1),(x_3,x_3-1,\cdots,x_2+1),\cdots,(n-1,n-2,\cdots,x_k+1),n 0,(x1,x11,,1),(x2,x21,,x1+1),(x3,x31,,x2+1),,(n1,n2,,xk+1),n。而 x i x_i xi 越小越好。考虑目前在 x i + 1 x_i+1 xi+1 楼,目前最高到达层数为 x i + 1 x_{i+1} xi+1 楼,如何找到 x i + 2 x_{i+2} xi+2

首先考虑 x i + 1 + 1 x_{i+1}+1 xi+1+1 楼能否跳上去。目前的攻击值已经达到了 ∑ j = 0 x i a j \displaystyle \sum_{j=0}^{x_{i}}a_j j=0xiaj,考虑二分答案,找到最近需要到 y y y 楼才能击杀 x i + 1 + 1 x_{i+1}+1 xi+1+1。显然楼层越高越能击杀怪物因为击杀到 x i + 1 x_{i}+1 xi+1 必然是从 y y y 楼一层层往下走的。找到这个 y y y 之后就需要检查从 x i + 1 + 2 x_{i+1}+2 xi+1+2 y y y 楼是不是都能顺利从 y y y 楼走下来,那么这就是刚刚的子问题,因而对于枚举的每一层,用同样的二分法可以找到满足最小的 y y y 使得从 y y y 开始一路往下可以一直杀到 x i + 1 + 1 x_{i+1}+1 xi+1+1。这时候这个 y y y 就是 x i + 2 x_{i+2} xi+2,检查能否从 x i + 1 x_{i}+1 xi+1 跳到 x i + 2 x_{i+2} xi+2 即可。时间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int t, n, k;
    scanf("%d", &t);
    while(t--)
    {
        long long base;
        scanf("%d%lld%d", &n, &base, &k);
        vector<long long> pre(n + 1), a(n + 1);
        for (int i = 1; i <= n;i++)
        {
            scanf("%lld", &a[i]);
            pre[i] = pre[i - 1] + a[i];
        }
        auto jump = [&](long long base, int target)
        {
            int left = target, right = n, ans = n + 1;
            while (left <= right)
            {
                int mid = (left + right) >> 1;
                if(base + pre[mid] - pre[target] >= a[target])
                {
                    ans = mid;
                    right = mid - 1;
                }
                else
                    left = mid + 1;
            }
            return ans;
        };
        bool flag = 1;
        int now = 0, up = 0;
        while(up < n)
        {
            int maximum = up + 1, low = up;
            while(low < maximum)
            {
                low++;
                int min_jump = jump(base, low);
                if (min_jump == n + 1)
                {
                    flag = 0;
                    break;
                }
                maximum = max(maximum, min_jump);
            }
            if(!flag)
                break;
            if(maximum > now + k)
            {
                flag = 0;
                break;
            }
            base += pre[maximum] - pre[up];
            now = up + 1;
            up = maximum;
        }
        if(flag)
            printf("YES\n");
        else
            printf("NO\n");
    }
    return 0;
}

I Fall with Full Star

题意:给定 n n n 个关卡,初始有一个能力值 s 0 s_0 s0,打完第 i i i 关可以使得能力值增加 d i d_i di,若增加完成后能力值大于等于 s i s_i si,则认为第 i i i 关满星通过。在每关最多只能打一次的情况下,至多可以满星通过多少关。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105 ∣ d i ∣ , ∣ s i ∣ ≤ 1 × 1 0 9 |d_i|,|s_i| \leq 1\times 10^9 di,si1×109

解法:首先一个最显然的贪心,将所有关卡按照 d i d_i di 的正负分成两类, d i d_i di 为正的一定先打, d i d_i di 为负的一定后打。

考虑 d i d_i di 为非负的贪心。对于第 i i i 关,为了让它满星通关,在打这关之前至少需要 s i − d i s_i-d_i sidi 的能力值。由于现在全正,能力值单调不减,因而首先按照 s i − d i s_i-d_i sidi 从小到大进行排序。但是这样还可能会导致有些关卡的 s i − d i s_i-d_i sidi 过高无法达到,提前吃掉 d i d_i di 放弃本关,而让前面本来不能通关的通关反而更优。因而,如果打到第 i i i 关,那么在排序中比它下标小的一定是都打过的。

维护一个堆 E E E 来记录可能牺牲(不满星通关),但是通过一定的顺序可以不牺牲的关卡, n o w \rm now now 为明确牺牲,绝对不可能通过调整顺序满星通过关卡的 d i d_i di 和。且这部分必然放弃关卡一定是放在最前面打。对于 E E E 中关卡,按照 d i d_i di 从大到小排序。考虑从后( s i − d i s_i-d_i sidi 大的关卡)往前的放弃关卡。考虑第 i i i 关能否利用明确牺牲的所有的关卡和前 i − 1 i-1 i1 的关卡获得的 d i d_i di 增量达成满星通关,分两种情况:

  1. 如果可以利用现有条件,那么不需要在 i i i 这里多牺牲关卡,即满星通过关卡增加。在这种情况下,在其余关卡都不变, d i > d j d_i > d_j di>dj 时,是可以将 i i i 牺牲掉换 j ( j > i ) j(j>i) j(j>i) 的不牺牲的。这是由于在判定 j j j 是否可以不牺牲的时候,其实是已经打了 i i i 了。能入堆的条件为可以满星通过 j j j,则此时如果选择牺牲 i i i 而打 j j j 也是可以办到的。因而满星通关数目增加 1 1 1,同时 i i i 入堆。
  2. 如果不能,那么还需要牺牲关卡。考虑当前堆中 d d d 最大的可以不牺牲的关卡 j ( j > i ) j(j>i) j(j>i),则有 s j ≤ d j + n o w + ∑ k = 0 j − 1 d k \displaystyle s_j \leq d_j+{\rm now}+\sum_{k=0}^{j-1} d_k sjdj+now+k=0j1dk,显然此时 i i i 已经计入了这个不等式中,因而如果不考虑最优,是一定可以救回来的。考虑两种子情况:
    1. d i > d j d_i>d_j di>dj。由于对于确认牺牲的关卡,评判其好坏的唯一标准就是 d d d 大。因而直接牺牲 i i i
    2. d i ≤ d j d_i \leq d_j didj。此时,牺牲 j j j 也可以换回来 i i i。首先由 j j j 可以进队有 s i − d i ≤ s j − d j , ∑ k = 0 j − 1 d k + n o w ≥ s j − d j \displaystyle s_i-d_i\leq s_j-d_j,\sum_{k=0}^{j-1}d_k+{\rm now} \geq s_j-d_j sidisjdj,k=0j1dk+nowsjdj。考虑距离 i i i 最近的上一个进堆 E E E k ( k ≤ j ) k(k \leq j) k(kj),有 ∑ l = 0 k − 1 d l + n o w ′ ≥ s k − d k \displaystyle \sum_{l=0}^{k-1}d_l+{\rm now'} \geq s_k-d_k l=0k1dl+nowskdk,其中 n o w ′ {\rm now'} now n o w {\rm now} now 加上了 [ k + 1 , j − 1 ] [k+1,j-1] [k+1,j1] 中必然放弃关卡的 d d d 之和。进一步转移到 i i i,由于 [ i + 1 , k − 1 ] [i+1,k-1] [i+1,k1] 区间全部放弃,那么当枚举量从 k k k 转移到 i i i 时, n o w ′ {\rm now'} now 还要增补 ∑ l = i + 1 k − 1 d l \displaystyle \sum_{l=i+1}^{k-1} d_l l=i+1k1dl。同时 s i − d i ≤ s k − d k s_i-d_i \leq s_k-d_k sidiskdk,那么有 ∑ l = 0 i − 1 d l + d j + ( n o w ′ + ∑ l = i + 1 k − 1 d l ) ≥ s k − d k ≥ s i − d i \displaystyle \sum_{l=0}^{i-1}d_l+d_j+\left({\rm now'}+\sum_{l=i+1}^{k-1}d_l \right )\geq s_k-d_k \geq s_i-d_i l=0i1dl+dj+(now+l=i+1k1dl)skdksidi。既然可以拿 i i i j j j 的不牺牲,那么谁的 d d d 大谁更优。

注意:上文中的 n o w \rm now now 变量在程序中是随着 i i i 的前移而不断变化的,但是在题解中,每一处的 n o w \rm now now 均为定值,是一个只与当前枚举的范围有关的一个函数。

对于 d i d_i di 为负数的情况,考虑倒过来执行整个操作,即从分数最低开始一点点涨上去,则过程与上面非负的情况完全相同。

因而总时间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)

#include<bits/stdc++.h>
#define IL inline
#define LL long long
using namespace std;
const int N=1e5+3;
const LL inf=1e18;
priority_queue<LL>q;
struct hh{
    LL d,s;
}a[N],b[N];
int n,na,nb;
LL s0,lia[N],lib[N];
bool cmpa(const hh &a,const hh &b){
    return a.s-a.d<b.s-b.d;
}
bool cmpb(const hh &a,const hh &b){
    return a.s>b.s;
}
IL LL in(){
    char c;int f=1;
    while((c=getchar())<'0'||c>'9')
      if(c=='-') f=-1;
    LL x=c-'0';
    while((c=getchar())>='0'&&c<='9')
      x=x*10+c-'0';
    return x*f;
}
IL void solve(){
    LL x,y;int ans=0;
    n=in(),na=nb=0,s0=in();
    for(int i=1;i<=n;++i){
        x=in(),y=in();
        if(x<0) b[++nb]=(hh){x,y};
        else a[++na]=(hh){x,y};
    }
    sort(a+1,a+na+1,cmpa),sort(b+1,b+nb+1,cmpb);
    LL pre=0,now=s0;
    while(q.size()) q.pop();
    for(int i=1;i<=na;++i) pre+=a[i].d,lia[i]=a[i].s-pre;
    for(int i=na;i;--i){
        s0+=a[i].d;
        if(now>=lia[i]) ++ans,q.push(a[i].d);
        else if(!q.size()||q.top()<a[i].d) now+=a[i].d;
        else now+=q.top(),q.pop(),q.push(a[i].d);
    }
    pre=0,now=s0;
    while(q.size()) q.pop();
    for(int i=1;i<=nb;++i) pre+=b[i].d,lib[i]=b[i].s-pre;
    for(int i=1;i<=nb;++i){
        if(now>=lib[i]) ++ans,q.push(-b[i].d);
        else if(!q.size()||q.top()<-b[i].d) now-=b[i].d;
        else now+=q.top(),q.pop(),q.push(-b[i].d);
    }
    printf("%d\n",ans);
}
int main()
{
    int T=in();
    while(T--) solve();
    return 0;
}

K Link is as bear

题意:给定一个序列 { a n } \{a_n\} {an},每次可以选定一个连续子区间 [ l , r ] [l,r] [l,r],使 a i ← ⨁ j = l r a j \displaystyle a_i \leftarrow \bigoplus_{j=l}^r a_j aij=lraj(同时变化),且保证存在 x ≠ y x \neq y x=y a x = a y a_x=a_y ax=ay,问当整个序列完全相同的时候,这个相同的最大值为多少。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105 a i ≤ 1 × 1 0 15 a_i \leq 1\times 10^{15} ai1×1015

解法:本题和“给定序列 { a n } \{a_n\} {an},找到异或和最大的子序列”完全等价,因而使用线性基即可。

下证两问题等价:

记要选择的数字为 1 1 1,不选择的数字为 0 0 0。分以下几种情况:

  1. 原序列中出现连续的两个 0 0 0,即连续两个都不选,记位置为 i , i + 1 i,i+1 i,i+1。连续执行两次操作 [ i , i + 1 ] [i,i+1] [i,i+1],即可消除这两个数字。
  2. 原序列中出现两个连续的 1 1 1,即连续两个都选,记位置为 i , i + 1 i,i+1 i,i+1。若接下来的数字不选,则可以执行 [ i , i + 1 ] [i,i+1] [i,i+1],然后将第一个数字(此时为 a i ⊕ a i + 1 a_i \oplus a_{i+1} aiai+1)保留下来,后面两个消除。 i − 1 i-1 i1 不选同理。

因而出现两个连续选择或者连续不选择的都可以使得两问题等价。考虑原序列形如 10101 10101 10101 或者 01010 01010 01010 这种完全交错形,但是注意题目中有条件 ∃ x ≠ y \exist x \neq y x=y 使得 a x = a y a_x=a_y ax=ay,考虑以下两种情况:

  1. x , y x,y x,y 同奇偶,即表示都不选或者都选。此时 a x a_x ax a y a_y ay 可以同时选和同时不选,所以可以作为 11 11 11 00 00 00,两种总有之一可以保证不是交错的。(感谢 Fried Chicken 的指正)
  2. x , y x,y x,y 不同奇偶。则选择 a x a_x ax 或者 a y a_y ay 可以将他们对应的 10 10 10 状态对调来创造连续的 1 1 1 或者 0 0 0

因而原问题与“给定序列 { a n } \{a_n\} {an},找到异或和最大的子序列”完全等价。

#include <bits/stdc++.h>
using namespace std;
long long p[70];
bool insert(long long x)
{
    for (int i = 60; i >= 0;i--)
    {
        if(!(x>>i))
            continue;
        if(!p[i])
        {
            p[i] = x;
            return 1;
        }
        else
            x ^= p[i];
    }
    return 0;
}
int main()
{
    long long x;
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        for (int i = 0; i <= 60;i++)
            p[i] = 0;
        for (int i = 1; i <= n;i++)
        {
            scanf("%lld", &x);
            insert(x);
        }
        long long ans = 0;
        for (int i = 60; i >= 0; i--)
            if ((ans ^ p[i]) > ans)
                ans ^= p[i];
        printf("%lld\n", ans);
    }
    return 0;
}
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值