Educational CF Round 135 题解

A题

签到题,找最大的那堆就行了

B题

签到题,要让最后剩下的后缀和最大,首先n应该在最后,那么n前面最大的子段和只能是n1,因此答案一定是2n1。接下来重新排列前面的部分,保证在n1之前的子段和是0就行了。那么不妨让它每隔一个就是0,那么每两个数翻转一下——2,1,4,3...(偶数个)或1,3,2,5,4...(奇数个)

C题

题意f(x)表示x在十进制下的位数。有两个数组ab,每次操作可以将ai变成f(ai)bi变成f(bi)。当两个数组按大小排序能变成相同数组时,称两个数组相似。问原始的ab数组经过最少多少次操作能变成相似。
分析:首先这两个数组一定可以相似,因为所以数字最终通过f函数都能变成1。那么可以每次取两个数组里的最大值,如果相等那么这两个元素可以匹配,并删除。否则一定有某一个最大值找不到匹配,可以应用f(x)将其变小,插入到原数组中,直到变空。

核心代码
std::multiset<int> a, b;
 
void Main()
{
    int n;
    read(n);
    for (int i = 1, x; i <= n; ++i)
        read(x), a.insert(x);
    for (int i = 1, x; i <= n; ++i)
        read(x), b.insert(x);
    int ans = 0;
    while (!a.empty())
    {
        if (*a.rbegin() == *b.rbegin())
        {
            a.erase(std::prev(a.end())), b.erase(std::prev(b.end()));
            continue;
        }
        else
        {
            ++ans;
            if (*a.rbegin() > *b.rbegin())
            {
                int x = *a.rbegin();
                a.erase(std::prev(a.end()));
                a.insert(log10(x) + 1);
            }
            else
            {
                int x = *b.rbegin();
                b.erase(std::prev(b.end()));
                b.insert(log10(x) + 1);
            }
        }
    }
    printf("%d\n", ans);
}

D题

题意:A和B从一个字符串s里取字符,每次只能从两端取,取出的字符放到自己字符串的前面。最终字典序小的那个人胜。字符串长度不超过2000

分析
经典博弈论区间DP题型。
比较字符串的字典序就是找到更小的前缀,而字符串的前缀总是从更小的区间得到。
dp[i][j]表示区间[i,j]的字符串,当前对A最优的结果。那么这一轮A的决策是取s[i]或者s[j]
假设A取了s[i],那么B面对的局面是[i+1,j],B的决策是取s[i+1],和s[j];下一轮留给A的局面分别是[i+2,j][i+1,j1]。因为A和B都是按最优方法执行,因此A如果取s[i],则下一轮A面对的局面一定是[i+2,j][i+1,j1]之间的最差局面。
也就是:
worse(add(dp[i+1][j1],s[i]<s[j]),add(dp[i+2][j],s[i]<s[i+1]))(取s[i]
add函数表示将两个比较结果合并的结果。
s[j]的情况同理。对于A本身来说,肯定是选择两个决策中最好的那个,因此状态转移方程是:

dp[i][j]=better(worse(add(dp[i+1][j1],s[i]<s[j]),add(dp[i+2][j],s[i]<s[i+1])),worse(add(dp[i+1][j1],s[j]<s[i]),add(dp[i][j2],s[j]<s[j1])))


时间复杂度O(n2)

核心代码
const int MAXN = 2000;
int dp[MAXN + 5][MAXN + 5];
char s[MAXN + 5];
 
int worst(int x, int y)
{
    if (x == 0 || y == 0)
        return 0;
    if (x == 2 || y == 2)
        return 2;
    return 1;
}
 
int better(int x, int y)
{
    if (x == 1 || y == 1)
        return 1;
    if (x == 2 || y == 2)
        return 2;
    return 0;
}
 
int add(int x, int y)
{
    if (x == 1)
        return 1;
    if (x == 0)
        return 0;
    return y;
}
 
int cmp(char x, char y)
{
    if (x < y)
        return 1;
    if (x > y)
        return 0;
    return 2;
}
 
void Main()
{
    scanf("%s", s + 1);
    memset(dp, -1, sizeof dp);
    int n = strlen(s + 1);
    for (int i = 1; i <= n - 1; ++i)
        dp[i][i + 1] = s[i] == s[i + 1] ? 2 : 1;
    for (int len = 4; len <= n; len += 2)
    {
        for (int i = 1; i <= n && (i + len - 1) <= n; ++i)
        {
            int j = i + len - 1;
            dp[i][j] = 0;
            // choose i
            int tmp1 = worst(add(dp[i + 2][j], cmp(s[i], s[i + 1])), add(dp[i + 1][j - 1], cmp(s[i], s[j])));
            int tmp2 = worst(add(dp[i + 1][j - 1], cmp(s[j], s[i])), add(dp[i][j - 2], cmp(s[j], s[j - 1])));
            dp[i][j] = better(tmp1, tmp2);
            // printf("dp[%d][%d] = %s\n", i, j, dp[i][j] == 0 ? "Lose" : (dp[i][j] == 1 ? "Win" : "Draw"));
        }
    }
    if (dp[1][n] == 2)
        printf("Draw\n");
    else if (dp[1][n] == 0)
        printf("Bob\n");
    else
        printf("Alice\n");
}
 

E题

题意
n道菜,第i道菜放黑胡椒的美味度是a[i],放白胡椒的美味度是b[i]。有m个商店出售胡椒包,第i个商店的黑胡椒包有x[i]份黑胡椒,白胡椒包有y[i]份白胡椒。对于每个商店,如果只在该商店购买胡椒包,且完全不浪费的情况下(购买u份黑胡椒包和v份白胡椒包,使得ux[i]+vy[i]=n),做菜的美味度最大是多少。n,m3105

分析
看到商店胡椒包关于不浪费的描述,很显然能想到扩展欧几里得,这样可以得到在该商店购买胡椒包是否可行。如果可行,方程ux[i]+vy[i]=n的正整数通解就是购买方案,接下来就是从这些方案里找到美味度最大的那个方案。

假设一共要做x道白胡椒菜,那么一共要做nx道黑胡椒菜。
美味度S(x)=i=1xbi+j=1nxaj
展开之后变成
S(x)=i=1x(biai)+i=1nai
这个式子后半部分是定值,所以只需要最大化前半部分就可以了,发现这就是将biai从大到小排序之后的前缀和。
还可以发现,由于将biai排序,所以S(x)是单峰函数,峰值在前缀和最大值处。

回到问题本身,现在有一系列v的通解形式和一个对于vy[i]的单峰函数。找到最大值只需要考虑两个位置:以峰值为分界线,前半峰里面v的通解的中vy[i]最大值以及后半峰里面v的通解中vy[i]的最小值。

核心代码
const int MAXN = 3E5;
int diff[MAXN + 5];
ll sum[MAXN + 5];

int exgcd(int a, int b, ll &x, ll &y)
{
    if (b == 0)
        return x = 1, y = 0, a;
    int d = exgcd(b, a % b, x, y);
    int t = x;
    x = y;
    y = t - (a / b) * y;
    return d;
}
void Main()
{
    int n, m;
    read(n);
    ll tot = 0;
    for (int i = 1, a, b; i <= n; ++i)
    {
        read(a, b);
        tot += a;
        diff[i] = b - a;
    }
    std::sort(diff + 1, diff + n + 1, std::greater<int>());
    sum[0] = 0;
    for (int i = 1; i <= n; ++i)
        sum[i] = sum[i - 1] + diff[i];
    int mx = 0;
    for (int i = 1; i <= n; ++i)
        if (sum[i] > sum[mx])
            mx = i;
    read(m);
    for (int i = 1, a, b; i <= m; ++i)
    {
        read(a, b);
        ll x, y;
        int d = exgcd(a, b, x, y);
        if (n % d != 0)
            printf("-1\n");
        else
        {
            int s = n / d;
            x *= s, y *= s;
            x = (x % (b / d) + b / d) % (b / d);
            y = (n - 1ll * a * x) / b;
            if (y < 0)
            {
                printf("-1\n");
                continue;
            }
            int tmax = y / (a / d), ymin = y - tmax * (a / d);
            int bymin = b * ymin, bymax = b * y;
            ll ans = -1;
            if (mx <= bymin)
                ans = tot + sum[bymin];
            else if (mx >= bymax)
                ans = tot + sum[bymax];
            else
            {
                int k = 1ll * (mx - bymin) * d / (1ll * a * b);
                ans = std::max(tot + sum[bymin + 1ll * a * b / d * k],
                               tot + sum[bymin + 1ll * a * b / d * k + 1ll * a * b / d]);
            }
            printf("%lld\n", ans);
        }
    }
}

F题

题意
有一个长度为n的数组a,将其按某种顺序重新排列成数组p,生成数组b,使得b[i]p[i]的倍数且b[i]单调递增。求b[i]数组的和最小值。n1000

分析
其实这个题就是把每个a[i],让b[i]是它的一个倍数即b[i]=ka[i],且b[i]是唯一的。所谓按某种顺序重新排列a,让b单调递增,就是把b排个序就好了,所以这一句话的作用就在于揭示了b[i]是唯一的。
问题在于a[i]a[j]的倍数可能相同,即存在k1a[i]=k2a[j],但k1a[i]只能用一次。
不难想到网络流模型,由a[i]连向它所有的倍数ka[i],容量为1;源点向a[i]连边,容量为1;倍数ka[i]向汇点连边,容量为1,费用为ka[i]。跑一遍最小费用最大流即可。
这样建出来是一个二分图,左部有n个节点,右部最多有n2个节点,一共有n2条边。这样跑费用流是爆炸TLE的。
考虑费用流的过程,只有右部节点向汇点边有费用,那么按最短路增广的话肯定是优先费用小的,也就是增广的顺序右部节点按权值从小到大排序。
那么不妨将脱离费用流,直接按右部节点从小到大排序,做增广顺序,跑二分图匹配。匈牙利算法的时间复杂度是O(VE)=O(n4),还是严重超时。
其实这里左部和右部的节点数量差距很大,最大匹配也就是n,而对右部节点按顺序增广有很多冗余。不妨在匈牙利算法没有找到增广路时,保留访问标记。因为当没有找到增广路的时候,图的匹配没变化,当前访问节点i没有找到增广路,下一次访问节点i也不会找到增广路。这样的话每两次最大匹配+1的时候,访问O(n2)条边,那么总复杂度是O(ME)=O(n3),再加上图匹配实际跑起来的效率很高,其实可以通过这道题目。
其实还有很多种可能的优化,比如动态加点、动态删边等,这是一道上限很高的题目,因此它的难度分数在3100。

核心代码
constexpr int MAXN = 1E3;
std::vector<int> G[MAXN * MAXN + MAXN + 5];
bool vis[MAXN + 5];
int n, a[MAXN + 5], b[MAXN * MAXN + 5];
int link[MAXN + 5];
 
bool find(int u)
{
    for (int v : G[u])
    {
        if (!vis[v])
        {
            vis[v] = true;
            if (link[v] == 0 || find(link[v]))
                return link[v] = u, true;
        }
    }
    return false;
}
 
void Main()
{
    read(n);
    for (int i = 1; i <= n; ++i)
        read(a[i]);
    int cnt = 0;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            b[++cnt] = a[i] * j;
    std::sort(b + 1, b + cnt + 1);
    cnt = std::unique(b + 1, b + cnt + 1) - (b + 1);
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= n; ++j)
        {
            int u = std::lower_bound(b + 1, b + cnt + 1, a[i] * j) - (b);
            G[u].push_back(i);
        }
    }
    ll ans = 0;
    for (int i = 1; i <= cnt; ++i)
    {
        if (find(i))
        {
            ans += b[i]; 
            for (int j = 1; j <= n; ++j)
                vis[j] = false;
        }
    }
    printf("%lld\n", ans);
}

G题

题意:
在一个长度为[0,d]的线段上,有n盏灯,位置是l[i]。有m个感兴趣的点p[i]。一盏灯的功率是[0,d],如果一个位置y被位置为x,功率为p的灯照亮,那么有|yx|p
q次询问,每次询问都要加上一盏位置在f的灯,求将所有感兴趣的点照亮的灯的功率分配的方案数。询问之间互相独立。
4d3105;1n2105;1m16;1q5105

分析:
先不考虑加灯。
看到m这么小的取值,很自然地想到状态压缩。设dp[i][S]表示前i盏灯照亮集合S的灯的方案数,w[i][s]表示灯i照亮集合为S的方案数,那么有dp[i][S]=dp[i1][S]w[i][s0](S|s0=S)
这样做时间复杂度过高了。注意到一个很棘手的点在于,一个位置可能被重复照亮,这样在状态转移的时候不是很好枚举。
正难则反,不妨考虑一下容斥。
首先很显然,总方案数是(d+1)n
不合法的方案则是某几盏灯不亮,其他灯随意。假设dp[S]表示不亮的灯是S的方案数,那么最终总的不合法方案数是S=12m1(1)|S|dp[S],并且显然dp[0]=(d+1)n
那么最终答案就是S=02m1(1)|S|dp[S]
所以接下来考虑dp[S]的计算方法。
注意到一盏灯,其实只会照亮以该灯为中心,一段距离内的所有兴趣点。假设S里相邻的两个1是SiSi+1,那么这两个兴趣点不被照亮的方案一定是这两个兴趣点内所有的灯都没有覆盖到这两个点。
g[l][r]表示兴趣点lr内的灯没有覆盖到lr的方案数,这个数组可以在O(nm2)的时间内预处理出来。
为了方便计算,我们添加l[0]=inf,l[m+1]=inf两个点,那么dp[S]=i=1|S|1g[Si][Si+1]g[0][S1]g[S|S|][m+1]
这一步预处理时间复杂度为O(m2m)
现在考虑添加一个灯f
如果从头再做一次dp,时间复杂度显然无法接受。
注意到添加灯的时候,对g数组的修改可以在O(m2)的时间复杂度完成。那么有没有一种快速的方法求出S=02m1(1)|S|dp[S]的值呢?
事实上是存在的。类比背包问题,每个物体可选可不选,如果用状态压缩去算的话显然也是枚举每种集合。实际上我们可以一位一位地递推。

重新设置dp状态,设dp[i]为长度为i的二进制串,且第i位为1的方案总数,那么有递推式
dp[i]=j=0i1(1)dp[j]g[j][i]。注意到每次末尾多一个1的情况下,对前面的状态中-1的奇偶性会出现变化,因此这里直接多乘一个-1就可以了。
由于我们知道第0位和第m+1位肯定为1,那么dp[0]=1,最终答案就是dp[m+1]
这样做的话dp的时间复杂度是O(m2),在每次添加灯的情况下可以直接重新做dp,查询的时间复杂度O(qm2)
最终时间复杂度为O(nm2+qm2)

除此之外还有很多做法。例如官方题解中用莫比乌斯反演在添加一盏灯的时候直接对相关的dp[S]求出贡献。或者在添加一盏灯的时候,被它照亮的一定是中间的一段,那么前缀和后缀必须是被原有的灯照亮。因此可以利用容斥预处理前缀[1,l]和后缀[r,n]都被原有的灯照亮的方案数。
总的来说这道题还是很灵活的,充分利用了很多组合数学和动态规划中的知识。

核心代码
constexpr int MAXN = 2E5;
constexpr int MAXD = 3e5;
constexpr int MAXM = 16;
 
int g[MAXM + 5][MAXM + 5], sg[MAXM + 5][MAXM + 5], dp[MAXM + 5];
int l[MAXN + 5], p[MAXM + 5];
 
void Main()
{
    int d, n, m;
    read(d, n, m);
    for (int i = 1; i <= n; ++i)
        read(l[i]);
    for (int i = 1; i <= m; ++i)
        read(p[i]);
    std::sort(l + 1, l + n + 1);
    std::sort(p + 1, p + m + 1);
    p[0] = -d * 10, p[m + 1] = d * 10;
    ll all = 1;
    for (int i = 1; i <= n; ++i)
        all = (all * (d + 1)) % mod;
    for (int i = 0; i <= m + 1; ++i)
        for (int j = 0; j <= m + 1; ++j)
            g[i][j] = 1;
    for (int i = 1, L = 0; i <= n; ++i)
    {
        while (L <= m && p[L + 1] < l[i])
            ++L;
        int R = L + 1;
        for (int x = 0; x <= L; ++x)
            for (int y = R; y <= m + 1; ++y)
            {
                int dist = std::min(std::abs(l[i] - p[x]) - 1, std::abs(l[i] - p[y]) - 1);
                dist = std::min(d, dist);
                g[x][y] = 1ll * (dist + 1) * g[x][y] % mod;
            }
    }
    int q;
    read(q);
    while (q--)
    {
        int f, L = 0;
        read(f);
        while (L <= m && p[L + 1] < f)
            ++L;
        int R = L + 1;
        dp[0] = mod - 1;
        for (int x = 0; x <= L; ++x)
            for (int y = R; y <= m + 1; ++y)
            {
                int dist = std::min(std::abs(f - p[x]) - 1, std::abs(f - p[y]) - 1);
                dist = std::min(d, dist);
                sg[x][y] = g[x][y];
                g[x][y] = 1ll * (dist + 1) * g[x][y] % mod;
            }
        for (int i = 1; i <= m + 1; ++i)
        {
            dp[i] = 0;
            for (int j = 0; j <= i - 1; ++j)
                dp[i] = (dp[i] + mod - 1ll * dp[j] * g[j][i] % mod) % mod;
        }
        printf("%d\n", dp[m + 1]);
        for (int x = 0; x <= L; ++x)
            for (int y = R; y <= m + 1; ++y)
            {
                g[x][y] = sg[x][y];
            }
    }
}
posted @   KSYImba  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示