Atcoder Regular Contest 169 A~E

博客围绕多个算法题展开,涵盖图论、思维、dp等类型。详细阐述各题题意,如根据数组操作判断元素正负、序列分割求和等;给出分析思路,像建图计算深度、贪心策略等;还附上对应代码。最后提供学习交流QQ群,方便分享做题思路与技巧。

A.Please Sign(图论)

题意:

给定长度为nnn的数组aaa,长度为n−1n-1n1的数组ppp,现在执行无数次以下操作:

  • 对于2≤i≤n2 \le i \le n2in ,按顺序执行api=api+aia_{p_i}=a_{p_i}+a_iapi=api+ai

确定最后a1a_1a1是正数还是负数还是000

分析:

考虑所有iiipip_ipi建边,发现是一棵以111为根的树,再计算每个节点距离111的深度,同一深度的权重相同,因此需将每个深度的节点的权值和都相加,如果权值和为000 ,说明这一深度层无用,否则最终答案的正负取决于最深深度节点的权值和。

代码:

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 2.5e5 + 5;
const int inf = 0x3f3f3f3f;
LL n, a[N], p[N], f[N], dep[N], mx;
vector<int> e[N];

void dfs(int u) {
    mx = max(mx, dep[u]);
    for (auto v: e[u]) {
        dep[v] = dep[u] + 1;
        dfs(v);
    }
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 2; i <= n; i++)
        cin >> p[i], e[p[i]].push_back(i);
    dfs(1);
    for (int i = 2; i <= n; i++)
        f[dep[i]] += a[i];
    int flag = 0;
    for (int i = n; i; i--)
        if (f[i]) {
            flag = f[i] > 0 ? 1 : -1;
            break;
        }
    a[1] = flag * inf + a[1];
    if (a[1] > 0)
        cout << "+";
    else if (a[1] == 0)
        cout << "0";
    else
        cout << "-";
    return 0;
}

B.Subsegments with Small Sums(思维)

题意:

给出正整数sss,和数列aaa,记f(a)f(a)f(a)表示:将序列分成xxx块子序列,并保证每一块子序列的和都小于等于sss。记f(a)f(a)f(a)为其中最小的xxx

现在给定长度为nnn的数列aaa,求 ∑1≤l≤r≤n​f((al​,al+1​,⋯,ar​))∑1≤l≤r≤n​f((a_l​,a_{l+1}​,⋯,a_r​))1lrnf((al,al+1,,ar)).

分析:

考虑贪心,从左往右考虑这个序列,如果上一段能放下就放进上一段,否则新开一段。再考虑左端点相同时每个右端点的答案,设这个和为 flf_lfl。分为两种情况:

  • 只有一段,对答案的贡献为111
  • 有至少两段,但根据上面的贪心,它们第一段结束的位置均相同。设这个位置是xxx,那么满足r≥xr \ge xrx的区间可以看作[x,r][x,r][x,r]答案+1+1+1,即fl=fx+(n−l+1)f_l=f_x+(n-l+1)fl=fx+(nl+1)

求和的ans=∑i=1i=nfians=\sum\limits_{i=1}^{i=n}f_ians=i=1i=nfi

代码:

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
const int inf = 0x3f3f3f3f;
LL a[N], sum[N], f[N];

int main() {
    LL n, s;
    cin >> n >> s;
    for (int i = 1; i <= n; i++)
        cin >> a[i], sum[i] = sum[i - 1] + a[i];
    for (int i = n; i; i--) {
        int nxt = lower_bound(sum + 1, sum + n + 1, sum[i - 1] + s + 1) - sum;
        f[i] = f[nxt] + (n - i + 1);
    }
    LL ans = 0;
    for (int i = 1; i <= n; i++)
        ans += f[i];
    cout << ans << endl;
    return 0;
}

C.Not So Consecutive(dp)

题意:

给出长度为nnn的序列aaa,定义一个序列aaa是好的,要求:

  • 1≤ai≤n≤50001 \le a_i \le n \le 50001ain5000
  • 序列aaa中没有任何一个数字xxx的连续出现次数>x>x>x

现在给定一个长度为nnn的序列aaa,序列中的元素不是−1-11就是1−n1-n1n中的任意一个数,请将所有的−1-11替换成1−n1-n1n的任意数字,计算最后结果有多少不同的序列aaa是好的,并将结果对998244353998244353998244353进行取模。

分析:

f[i][j]f[i][j]f[i][j]表示填了前iii个位置,第jjj个位置填的数字是jjj的方案数。枚举这个连续段的起点,得到转移为:
fi,j=∑k=max(0,i−j)i−1[∀l∈[k+1,i],Al=−1orAl=j]∑col≠jfk,colf_{i,j}= \sum\limits_{k=max(0,i-j)}^{i-1}[∀l∈[k+1,i],A_l=−1 or A_l=j]\sum\limits_{col\neq j}f_{k,col}fi,j=k=max(0,ij)i1[l[k+1,i],Al=1orAl=j]col=jfk,col
方括号里的式子是为了找iii前面最后一个填了不为jjj 的数的位置,设这个位置为 lstlstlst。对每个数维护 posjpos_jposj 表示数jjj当前最后一次出现的位置。那么对于每个新的iii,我们记录 pospospos 的最大值和次大值。
同时将col≠jcol \neq jcol=j这个条件容斥掉,得到最后转移方程为:
fi,j=∑k=lsti−1∑col=1nfk,col−∑k=lsti−1fk,jf_{i,j}=\sum\limits_{k=lst}^{i-1} \sum\limits_{col=1}^{n} f_{k,col}-\sum\limits_{k=lst}^{i-1}f_{k,j}fi,j=k=lsti1col=1nfk,colk=lsti1fk,j
si,j=∑k=1ifk,js_{i,j}=\sum\limits_{k=1}^{i}f_{k,j}si,j=k=1ifk,j,sumi=∑j=1i∑col=1nfi,colsum_i=\sum\limits_{j=1}^{i} \sum\limits_{col=1}^{n}f_{i,col}sumi=j=1icol=1nfi,col
前缀和优化,n2n^2n2转移即可。

代码:

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 5e3 + 5;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
int pos[N], f[N][N], s[N][N], sum[N], n, a[N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    f[0][0] = 1, sum[0] = 1;
    for (int i = 1; i <= n; i++) {
        if (a[i] != -1)
            pos[a[i]] = i;
        int mx1 = 0, mx2 = 0;
        for (int j = 1; j <= n; j++) {
            if (pos[j] > mx1)
                mx2 = mx1, mx1 = pos[j];
            else if (pos[j] > mx2)
                mx2 = pos[j];
        }
        for (int j = 1; j <= n; j++) {
            int lst = max(i - j, (a[mx1] == j) ? mx2 : mx1);
            f[i][j] = sum[i - 1] - (lst ? sum[lst - 1] : 0) - (s[i - 1][j] - (lst ? s[lst - 1][j] : 0));
            f[i][j] = (f[i][j] % mod + mod) % mod;
            s[i][j] = (s[i - 1][j] + f[i][j]) % mod;
            (sum[i] += f[i][j]) %= mod;
        }
        (sum[i] += sum[i - 1]) %= mod;
    }
    cout << (sum[n] - sum[n - 1] + mod) % mod << endl;
    return 0;
}

D Add to Make a Permutation(思维)

题意:

给你一个序列aaa0≤ai≤n−10 \le a_i \le n-10ain1,你可以进行任意次以下操作:

  • 任意选择mmm个元素,把他们的值增加111并对nnn取模。

询问能否通过任意次操作使得aaa变成一个0−n−10-n-10n1的排列。

分析:

设序列 aaa 操作得到未被取模的最终序列 bbb

bbb一定满足以下条件:

  • bi≤aib_i \le a_ibiai

  • bi%nb_i \% nbi%n两两不同

  • s=∑i=1n(bi−ai)s=\sum\limits_{i=1}^{n}(b_i-a_i)s=i=1n(biai),有s%m=0s\%m=0s%m=0

  • max(bi−ai)≤smmax({b_i−a_i}) \le \frac{s}{m}max(biai)ms

在一定的操作次数下max(b∗i−ai)max({b*i−a_i})max(biai) 越小越容易满足条件。将aaa序列升序排序,最优的对应关系一定是将bbb也升序排序。

发现答案只和sss 的值有关,有结论:若有解,一定存在一种最优方案形如 b=(x,x+1,⋯,x+n)b=(x,x+1,⋯,x+n)b=(x,x+1,,x+n)

证明:假设我们有一个最优的 bbb 序列,且 bn−b1b_n-b_1bnb1>n。那么令 b1←bn−nb_1←b_n−nb1bnn,bn←b1+nb_n←b_1+nbnb1+n,依次对照上面的所有条件:

  • 因为 bn−n>b1b_n−n>b_1bnn>b1,新的 b1b_1b1 比原来大。同时因为 b1>a1b_1>a_1b1>a1 , an−a1<na_n − a_1 < nana1<n,有 b1+n>anb_1+n>a_nb1+n>an,满足条件111

  • 加减nnn不改变取模后的值。

  • 总和没变,S 不变。

  • bn−anb_n-a_nbnan 一定变小。前者有 bn−an>bn−n−a1b_n-a_n>b_n-n-a_1bnan>bnna1,所以也变小,满足条件。

那么我们只需考虑最小化xxx的值。根据第一个条件可以得到xxx的下界。第四个条件xxx每增加 1,不等式左侧增加111,右侧增加nm\frac{n}{m}mn。因为 n>mn>mn>m,第四个条件也为xxx提供了下界。
接下来令 xxx满足第三个条件即可,发现 xxxx+nx+nx+n 是等价的,xxx 可以直接枚举nnn次。

代码:

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
const int mod = 998244353;
LL n, m, a[N], sum, x;

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; i++)
        x = max(x, a[i] - i + 1);
    for (int i = 1; i <= n; i++)
        sum += (x + i - 1) - a[i];
    bool flag = 0;
    for (int i = 0; i <= n; i++)
        if ((sum + n * i) % m == 0) {
            x += i, sum += n * i, flag = 1;
            break;
        }
    if (!flag) {
        cout << -1 << endl;
        return 0;
    }
    LL mx = 0;
    for (int i = 1; i <= n; i++)
        mx = max(mx, x + i - 1 - a[i]);
    while (mx > sum / m)
        mx += m / __gcd(n, m), sum += n * m / __gcd(n, m);
    cout << sum / m << endl;
    return 0;
}

E Avoid Boring Matches(思维)

题意:

2n2^n2n个人,参加一项锦标赛。规则如下:

  • 每个人都有一顶颜色为红色或者蓝色的帽子,由字符串SSS给出。
  • 重复以下操作,直到只剩下一个参与者:假设2k2k2k是当前的参与者人数。将参与者分成两组。可以自由选择如何配对它们。 然后,每组举行一场比赛,赢家留下来,输家离开比赛。 参与者按强度降序编号,因此编号较小的参与者总是获胜。

两个戴着红帽子的参与者之间的比赛称为无聊的比赛。 你的目标是安排配对,这样在比赛期间就不会发生无聊的比赛。
现在你可以执行任意次以下操作:

  • 选择SSS中相邻的两个字符并交换他们。

询问是否有可能实现目标。 如果是,请查找所需的最小操作数。

分析:

RRRBBB 多一定无解,否则可以换成 BBBB...RRRBBBB...RRRBBBB...RRR的形式,一定有解。
先找判断序列合法的充要条件。考虑每轮的最优匹配策略,我们希望更多的 BBB能留到下一轮,因此要尽可能地让 BBB 和它后面的 RRR 配对。

在多种配对方案能保留的 $B $数量相同时,我们希望留下的 $B $位置尽可能靠前,因为这样在下下轮它们被保留下来的概率更大。因此我们得到这样的贪心策略:从左到右考虑每个 BBB,将它和右边第一个未被配对的 RRR配对。最终将所有剩下未配对的数两两配对。根据上文可以知道这样做是最优的。设 tit_iti表示长度为 2i2^i2i,且为所有 BBB尽可能靠右的合法解。

t0t_0t0=RRR,考虑从 ti−1t_{i−1}ti1 得到 tit_iti。从左到右处理 ti−1t_{i−1}ti1 的每一位,若当前位是 RRR,表示这位没被匹配,ti=ti+Rt_i=t_i+Rti=ti+R;当前位是 BBB,我们希望下一个BBB尽量靠后,即与之匹配的 RRR尽量更近,ti=ti+BRt_i=t_i+BRti=ti+BR。不足 2i2^i2i 位用 BBB 补齐。

tnt_ntn的第 jjjBBB 的位置为 TjT_jTj,原序列位置为 SjS_jSj。那么若存在 Sj>TjS_j>T_jSj>Tj,则该序列不合法。证明大概是如果一个 Sj<TjS_j < T_jSj<Tj,匹配数不会变多;但如果Sj>Tj,BRS_j > T_j,BRSj>Tj,BR的匹配数一定减少。

答案是令 SSS 满足上述条件的最小操作次数ans=∑j=12nmax(0,Tj−Sj)ans=\sum\limits_{j=1}^{2^n}max(0,T_j-S_j)ans=j=12nmax(0,TjSj)

代码:

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int inf = 0x3f3f3f3f;
const int N = (1 << 18) + 5;
int n;
string s, t[N];
int tot, pos[N];

int main() {
    cin >> n;
    cin >> s;
    t[0] = "R";
    for (int i = 1; i <= n; i++) {
        for (auto c: t[i - 1])
            if (c == 'R')
                t[i] += "R";
            else
                t[i] += "BR";
        while (t[i].size() < (1 << i))
            t[i] += "B";
    }
    for (int i = 0; i < (1 << n); i++)
        if (t[n][i] == 'B')
            pos[++tot] = i;
    tot = 0;
    long long ans = 0;
    for (int i = 0; i < (1 << n); i++)
        if (s[i] == 'B') {
            tot++;
            if (i > pos[tot])
                ans += i - pos[tot];
            if (tot == (1 << n - 1))
                break;
        }
    if (tot < (1 << n - 1))
        cout << -1 << endl;
    else
        cout << ans << endl;
    return 0;
}

学习交流

以下为学习交流QQ群,群号: 546235402,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值