2022“杭电杯” 中国大学生算法设计超级联赛(4)1 2 3 5 11题解

1001-Link with Bracket Sequence II

题目大意:
有一个长度为n的括号序列,一共有m种括号,序列中有若干个位置是空的,可以填上任意的括号。
合法的序列定义如下:

  • 空序列是合法的
  • A是合法序列,那么 (i A )i 也是合法序列(同类型的左括号和同类型的右括号匹配)
  • A、B是合法序列,那么AB也是合法序列

问给定的序列可以构成多少种合法序列。
输入的序列中,绝对值相等的为同一种类型的括号。正数表示左括号,复数表示右括号,0表示空。

思路:
对于一个长度大于2的合法括号序列,一定是(i A )i 或 AB的形式。
考虑用区间dp,dp[i][j]表示区间[i,j]的合法序列数量,状态转移方程如下:

  1. dp[i][j] = get(i,j) * dp[i+1][j-1] get(i,j)表示 ij 位置的括号能组成的合法括号对数
  2. dp[i][j] += dp[i][k] * dp[k+1][j]

但是第二个转移方程存在重叠问题,比如1 -1 0 0 0 0 2 -2这种。

考虑换一种枚举的方式,以右括号 j 的匹配左括号位置作为k进行枚举,就可以保证是不重叠的。

为了方便计算,可以定义两种状态:
g[i][j]表示区间[i,j]的合法序列数量
f[i][j]表示区间[i,j]ij位置的括号匹配的合法序列数量

状态转移方程如下:
f[i][j] = g[i + 1][j - 1] * get(i, j)
g[i][j] += g[i][k] * f[k+1][j]

需要注意的是,计算ij 位置的括号能组成的合法括号对数时要避免将左边是右括号,右边是左括号的情况算进去。

AC代码:

#include <bits/stdc++.h>
const int N = 5e2 + 5;
const long long mod = 1e9 + 7;
using namespace std;

long long a[N], f[N][N], g[N][N], n, m;

long long get(int l, int r)
{
    if (a[l] >= a[r]) //防止出现左边是右括号,右边是左括号的情况
    {
        if (a[l] == 0 && a[r] == 0) return m;
        if (a[l] == -a[r] || a[l] * a[r] == 0) return 1;
    }
    return 0;
}

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    if (n & 1) //如果长度是奇数的话无论如何都构不成合法序列
    {
        cout << "0" << endl;
        return;
    }
    for (int len = 2; len <= n; len += 2) //枚举区间长度
    {
        int maxl = n - len + 1;
        for (int l = 1; l <= maxl; l++)
        {
            int r = l + len - 1;
            f[l][r] = g[l + 1][r - 1] * get(l, r) % mod;
            g[l][r] = f[l][r];
            for (int k = l + 1; k <= r - 2; k += 2)
                g[l][r] = (g[l][r] + g[l][k] * f[k + 1][r] % mod) % mod;
        }
    }
    cout << g[1][n] << endl;
}
signed main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    for (int i = 1; i < N; i++)
        g[i][i - 1] = 1;
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

1002-Link with Running

题目大意:
给定一个图,每条边有两个权值e和p,从1出发到n,在保证e的权值和最小的情况下使得p值和最大。
输出e和p的结果。(e,p>=0)

思路:
图论大杂烩题。
如果只是求最小的e,因为没有负边权,直接用dijkstra跑就行。
现在考虑p,题目保证结果是存在的,因此在1到n的路径上不会存在e=0,p>0的环。但是会存在e=0,p=0的环。
一开始想的是跑dijkstra的时候如果e相同的时候p更大就更新,然后类似拓扑排序,将能到达y的所有点都更新后再将y入队。
但是这种做法在遇到e=0,p=0的环时会失效,只有在DAG上才能跑。
那么可以将最短路先提取出来,进行缩点,使得图变成一个DAG。
然后在缩点后的图上跑最长路即可,跑最长路的时候也是将到达y点的所有点更新后再将y入队,需要记录y的入度。
一共要建三个图,代码量比较大,需要注意初始化的时候不要漏了某些变量。

AC代码:

#include <bits/stdc++.h>
const long long inf = 1e18 + 7;
const int N = 3e5 + 5;
using namespace std;

long long n, m, din[N]; // din[i]记录强连通分量的入度

struct edge
{
    int to, next;
    long long e, p;
} e1[N], e2[N], e3[N];
int head1[N], head2[N], head3[N], ecnt1, ecnt2, ecnt3;

void add1(int u, int v, int e, int p) //初始图加边
{
    e1[++ecnt1] = {v, head1[u], e, p};
    head1[u] = ecnt1;
}
void add2(int u, int v, int e, int p) //最短路加边
{
    e2[++ecnt2] = {v, head2[u], e, p};
    head2[u] = ecnt2;
}
void add3(int u, int v, int e, int p) //缩点后的图加边
{
    e3[++ecnt3] = {v, head3[u], e, p};
    head3[u] = ecnt3;
    din[v]++; //入度+1
}

struct node
{
    long long dis, idx;
};
bool operator<(node a, node b) { return a.dis > b.dis; }
long long dis[N];
void dijkstra()
{
    priority_queue<node> q;
    long long d, id;
    for (int i = 1; i <= n; i++)
        dis[i] = inf;
    dis[1] = 0;
    q.push({0, 1});
    while (q.size()) //在第一个图上跑最短路
    {
        d = q.top().dis;
        id = q.top().idx;
        q.pop();
        if (d != dis[id]) continue;
        for (int i = head1[id]; i; i = e1[i].next)
        {
            int v = e1[i].to, e = e1[i].e, p = e1[i].p;
            if (d + e < dis[v])
            {
                dis[v] = d + e;
                q.push({dis[v], v});
            }
        }
    }
    for (int i = 1; i <= n; i++) //建第二个图,只保留最短路径
    {
        if (dis[i] == inf) continue;
        for (int j = head1[i]; j; j = e1[j].next)
        {
            int v = e1[j].to, e = e1[j].e, p = e1[j].p;
            if (dis[i] + e == dis[v])
                add2(i, v, e, p);
        }
    }
}

int dfn[N], low[N], group[N], cnt, tot;
stack<int> S;
void tarjan(int x) //求出给个点所属的强连通分量
{
    dfn[x] = low[x] = ++tot;
    S.push(x);
    for (int i = head2[x]; i; i = e2[i].next)
    {
        int v = e2[i].to;
        if (!dfn[v]) //这个点还没有被访问过
        {
            tarjan(v);
            low[x] = min(low[x], low[v]);
        }
        else if (!group[v]) //这个点不属于任何一个连通集(说明还在栈中)
            low[x] = min(low[x], dfn[v]);
    }
    if (low[x] == dfn[x]) //该顶点为连通集的第一个被访问元素
    {
        cnt++; //连通集个数加一
        while (S.top() != x)
        {
            group[S.top()] = cnt; //记录所属的连通集
            S.pop();
        }
        group[x] = cnt;
        S.pop();
    }
}

void shrink() //强连通分量缩点
{
    tarjan(1); //求出给个点所属的强连通分量

    for (int i = 1; i <= n; i++) //建第三个图,强连通分量之间连边
    {
        for (int j = head2[i]; j; j = e2[j].next)
        {
            int v = e2[j].to;
            if (group[i] != group[v]) //位于两个不同的强连通分量中
                add3(group[i], group[v], e2[j].e, e2[j].p);
        }
    }
}

long long dp[N]; //最大p
void getmaxp()   //跑最长路
{
    for (int i = 1; i <= cnt; i++)
        dp[i] = -inf;
    queue<int> q;
    dp[group[1]] = 0;
    q.push(group[1]);
    while (q.size())
    {
        int x = q.front();
        q.pop();
        for (int i = head3[x]; i; i = e3[i].next)
        {
            int v = e3[i].to;
            dp[v] = max(dp[v], dp[x] + e3[i].p);
            if (--din[v] == 0) q.push(v); //将该点的入点都更新完之后再将该点入队
        }
    }
}
void init()
{
    ecnt1 = ecnt2 = ecnt3 = 0;
    cnt = tot = 0;
    while (S.size())
        S.pop();
    for (int i = 0; i <= n; i++)
    {
        head1[i] = head2[i] = head3[i] = 0;
        group[i] = dfn[i] = low[i] = 0;
        din[i] = 0;
    }
}
void solve()
{
    int u, v, e, p;
    cin >> n >> m;
    init();
    while (m--)
    {
        cin >> u >> v >> e >> p;
        add1(u, v, e, p);
    }
    dijkstra();
    shrink();
    getmaxp();
    cout << dis[n] << " " << dp[group[n]] << endl;
}

signed main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

1003-Magic

题目大意:
有若干个魔法塔,编号为1…n。
激活每个魔法塔最少需要pi点能量。
一次充能操作为选择一个魔法塔 i ,使得区间[i-k+1, i+k-1]内的魔法塔能量值加1。
充能操作存在限制,[li, ri]区间内最多进行b次充能。
问是否存在解,如果存在,输出最少的充能次数。

思路:
要输出最少的次数,又存在大小限制,如果能想到用差分约束来做这道题的话就很简单了。
为了便于建图,用a[i]表示区间[0, i]内的充能次数。
一共是三种约束条件:

  • 激活魔法塔最低能量:a[i+k-1] - a[i-k] >= p[i] => a[i-k] - a[i+k-1] <= -p[i]
  • 区间内操作次数限制:a[r] - a[l-1] <= b
  • 前缀和自身的约束:a[i-1] - a[i] <=0

然后将dis[0]置为0后跑spfa即可,跑出来的dis是负值,最后要乘-1。

AC代码:

#include <bits/stdc++.h>
const int inf = 1e9 + 7;
const int N = 1e4 + 5;
using namespace std;

struct edge
{
    int to, next, w;
} e[N * 10];
int head[N], ecnt;
void add(int u, int v, int w) { e[++ecnt].next = head[u], e[ecnt].to = v, e[ecnt].w = w, head[u] = ecnt; }
void einit(int n)
{
    ecnt = 0;
    for (int i = 0; i <= n; i++)
        head[i] = 0;
}

int n, k, dis[N], cnt[N];
bool vis[N];
bool spfa(int start = 0) //算法主体
{
    for (int i = 0; i <= n; i++) // cnt记录松弛次数,vis记录是否入队
    {
        dis[i] = inf;
        vis[i] = cnt[i] = 0;
    }
    dis[start] = 0;
    queue<int> q;
    q.push(start);

    vis[start] = 1;
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for (int i = head[u]; i; i = e[i].next)
        {
            int v = e[i].to;
            int w = e[i].w;
            if (dis[v] > dis[u] + w)
            {
                dis[v] = dis[u] + w;
                if (!vis[v])
                {
                    q.push(v);
                    vis[v] = 1;
                }
                if (++cnt[v] >= n) return false;
            }
        }
    }
    return true; //没有负环,返回true
}

void solve()
{
    int p, q, l, r, b;
    cin >> n >> k;
    einit(n);
    for (int i = 1; i <= n; i++) //魔法塔最少能量约束
    {
        cin >> p;
        add(max(i - k, 0), min(i + k - 1, n), -p);
    }
    cin >> q;
    for (int i = 1; i <= q; i++) //区间和约束
    {
        cin >> l >> r >> b;
        add(r, l - 1, b);
    }
    for (int i = 1; i <= n; i++) //前缀和自身的约束,后面的不小于前面的
        add(i - 1, i, 0);
    if (spfa())
        cout << -dis[n] << endl;
    else
        cout << "-1" << endl;
}
signed main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

1005-Link with Level Editor II

题目大意:
有若干个图和若干个点,每个图中的点是相同的,但是边不同。在一个图中可以选择从当前结点走向相邻的结点,也可以选择留在当前结点。初始时的结点为1,终点为m。(m<=20)
从给定的图序列中求出最长的子序列长度,使得子序列从1走到m的路径数不大于k。

思路:
因为每个图中最多只有20个点,要求出从一个点到另一个点的路径数,可以用矩阵乘法来快速求得。
同时子序列具有单调性,当左边界确定后,移动右边界只会使得路径数更多,因此可以用双指针。
矩阵是满足结合率的,可以预处理一段区间的乘积,但是在移动左边界指针时,要将最左侧的矩阵除去却是很困难的。
因此要设法规避除法的操作,这里用了一个技巧:对顶栈。
先介绍一个变量limit,表示当前预处理的区间乘积右端点都是到limit
然后是双指针lr表示当前区间的左右端点。
cur变量表示区间[limit, r]的乘积。
当移动r时,只要将cur乘上矩阵r即可。然后将cur与预处理的llimit的区间乘积相乘即可得到1到m的路径数。
移动l时,判断l是否大于limit,如果大于limit,就将limit赋值为r,重新计算从llimit的所有区间乘积,因为l只会右移,因此原来预处理范围内的区间乘积不会再用到了。
时间复杂度为O(nm3)

由于矩阵不满足交换率,在计算矩阵乘积的时候一定要注意计算顺序。

AC代码:

#include <bits/stdc++.h>
const long long inf = 1e9 + 7;
const int N = 5e3 + 5;
const int M = 21;
using namespace std;
int n, m, k;
struct Matrix //行和列相同的矩阵
{
    long long num[M][M];
    Matrix(long long x = 0) //初始化为x单位矩阵
    {
        memset(num, 0, sizeof(num));
        for (int i = 1; i <= m; i++)
            num[i][i] = x;
    }
    Matrix operator*(const Matrix &that) const
    {
        Matrix res;
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= m; j++)
                for (int k = 1; k <= m; k++)
                    res.num[i][j] = min(inf, res.num[i][j] + num[i][k] * that.num[k][j]);
        return res;
    }
};

Matrix a[N], b[N];

bool check(Matrix &x, Matrix &y)
{
    long long cnt = 0;
    for (int i = 1; i <= m; i++)
        cnt = min(inf, cnt + x.num[1][i] * y.num[i][m]);
    return cnt <= k;
}

void solve()
{
    int u, v;
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++)
    {
        int l;
        cin >> l;
        a[i] = 1; //初始化为1矩阵,因为可以留在原地
        while (l--)
        {
            cin >> u >> v;
            a[i].num[u][v] = 1;
        }
    }
    int ans = 0;
    Matrix cur = 1;
    b[0].num[1][m] = inf; //防止将b[0]算入
    int l = 0, lim = 0, r = 1;
    while (r <= n)
    {
        cur = cur * a[r];
        while (!check(b[l], cur))
        {
            l++;
            if (l > lim) // 当前栈中维护的从[l,lim]到lim的区间乘积值,l超过lim时就要重新计算
            {
            	//新的区间为[lim,r]到r
                b[r] = a[r];
                for (int i = r - 1; i > lim; i--)
                    b[i] = a[i] * b[i + 1]; //注意两者相乘的顺序,a要在b的前面,因为矩阵不满足交换律
                lim = r;
                cur = 1;
            }
        }
        ans = max(ans, r - l + 1);
        r++;
    }
    cout << ans << "\n";
}
signed main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

1011-Link is as bear

题目大意:
给定一个序列,每次可以选择一个区间,使得区间内的数字变成区间的异或和。
经过一定次数的操作后所有的数字可以变成相同的,求最后数字最大的值。

思路:
首先给出结论:答案就是从序列中取出任意数字进行异或。
有了这个结论后直接套线性基的板子就行。
这个结论更多还是靠猜,比赛的时候没必要推。
下面是题解中给的证明:
在这里插入图片描述
AC代码:

#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 1e5 + 5;
using namespace std;

long long a[N], d[61];
void add(long long x) //插入线性基
{
    for (int i = 60; i >= 0; i--)
    {
        if (x & (1ll << i))
        {
            if (d[i])
                x ^= d[i];
            else
            {
                d[i] = x;
                break;
            }
        }
    }
}

void solve()
{
    int n;
    long long ans = 0;
    mem(d, 0);
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
        add(a[i]);
    }
    for (int i = 60; i >= 0; i--)
        if ((ans ^ d[i]) > ans) ans ^= d[i];
    cout << ans << "\n";
}
signed main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值