2021“MINIEYE杯”中国大学生算法设计超级联赛 第三场题解

2021“MINIEYE杯”中国大学生算法设计超级联赛   第三场题解

赛后深知自己水平提高空间很大

6979 Photoshop Layers

题意:

给定一些RGB编码的颜色,他们有两种操作:

1:直接覆盖之前的颜色

2:同之前的颜色进行累加 与255取最小值

他有一堆询问,询问 L-R 代表从L颜色到R颜色进行操作后的颜色是什么

思路:
思考思考我们能够发现:

1、由1颜色定义可知道如果当前下标R的颜色操作就是1的话,那么最后的颜色就是R下标颜色

2、根据1我们可知,当前R只会与最近的那个1操作有关,剩下的就是累加2操作了

那么我们可以对于每一个当前下标idx维护两个值:

①代表离当前下标idx最近的那个1操作

②当前RGB颜色相对于最近1操作的那个前缀和

这样对于每一个询问,只需先判断L和维护的那个idx的位置关系:

①如果L <= idx,说明当前前缀和的值就是答案

②否则,需要Sum[R] - Sum[L - 1]进行差分得到一段区间的RGB颜色~(注意相减后需要与255进行比较)

代码:

点击查看代码
#include <iostream>
using namespace std;
const int MAXN = 100005;
struct node
{
    int r,g,b;
    int idx;//离他最近的那个m = 1 
    node()
    {r = g = b = 0;}
    node operator - (const node &a)    const
    {
        node now;
        now.r = min(r - a.r,255);
        now.g = min(g - a.g,255);
        now.b = min(b - a.b,255);
        now.idx = idx;
        
        return now;
    }
    void ok()
    {
        r = min(r,255);
        g = min(g,255);
        b = min(b,255);
    }
};
node Get(string s)
{
    node now;
    int x = 0;
    for(int i = 0;i < 6;i+=2){
        int k1,k2;
        if(s[i] >= 'A')
        k1 = s[i] - 'A' + 10;
        else
        k1 = s[i] - '0';
        
        if(s[i + 1] >= 'A')
        k2 = s[i + 1] - 'A' + 10;
        else
        k2 = s[i + 1] - '0';
        
        x = k1*16 + k2;
        if(i == 0)
        now.r = x;
        else if(i == 2)
        now.g = x;
        else if(i == 4)
        now.b = x; 
    }
    return now;
}

void Go(node &a,node b,string s)
{//将b + s放入a中 
    for(int i = 0;i < 6;i+=2)
    {
        int k1,k2;
        if(s[i] >= 'A')
        k1 = s[i] - 'A' + 10;
        else
        k1 = s[i] - '0';
        
        if(s[i + 1] >= 'A')
        k2 = s[i + 1] - 'A' + 10;
        else
        k2 = s[i + 1] - '0';
        
        int x = k1*16 + k2;
        if(i == 0)
        a.r = b.r + x;
        else if(i == 2)
        a.g = b.g + x; 
        else if(i == 4)
        a.b = b.b + x;
    }
}
node sum[MAXN];
void print(node now)
{
    now.ok();
    char ans[10];
    int cnt = 0;
    int x = now.r/16;
    int y = now.r%16;
    if(x >= 10)
    {
        x -= 10;
        ans[++cnt] = 'A' + x;
    }
    else
    ans[++cnt] = x + '0';
    
    if(y >= 10)
    {
        y -= 10;
        ans[++cnt] = 'A' + y;
    }
    else
    ans[++cnt] = y + '0';
    
    x = now.g/16;
    y = now.g%16;
    if(x >= 10)
    {
        x -= 10;
        ans[++cnt] = 'A' + x;
    }
    else
    ans[++cnt] = x + '0';
    
    if(y >= 10)
    {
        y -= 10;
        ans[++cnt] = 'A' + y;
    }
    else
    ans[++cnt] = y + '0';
    
    x = now.b/16;
    y = now.b%16;
    if(x >= 10)
    {
        x -= 10;
        ans[++cnt] = 'A' + x;
    }
    else
    ans[++cnt] = x + '0';
    
    if(y >= 10)
    {
        y -= 10;
        ans[++cnt] = 'A' + y;
    }
    else
    ans[++cnt] = y + '0';
    
    for(int i = 1;i <= cnt;i++)
    printf("%c",ans[i]);
    puts("");
}
char ss[10]; 
int main()
{
    int t;
    scanf("%d",&t);
    sum[0].r = sum[0].g = sum[0].b = 0;
    sum[0].idx = 0;
    while(t--)
    {
        int n,q;
        scanf("%d %d",&n,&q);
        for(int i = 1;i <= n;++i)
        {
            int m;
            string s;
            scanf("%d  %s",&m,ss);
            s = ss;
            if(m == 1)
            {
                sum[i] = Get(s);
                sum[i].idx = i;
            } 
            else
            {
                Go(sum[i],sum[i - 1],s);
                sum[i].idx = sum[i - 1].idx;
            }
        }
        while(q--)
        {
            int le,ri;
            scanf("%d %d",&le,&ri);
            if(sum[ri].idx >= le)
            {//说明就是当前的前缀和了 
                print(sum[ri]);
            }
            else
            {//需要相减 
                print(sum[ri] - sum[le - 1]);
            }
        }
    }
    return 0;
}
/*
1
5 5
1 64C832
2 000100
2 010001
1 323C21
2 32C8C8

2 5


*/

QAQ因为是比赛时的代码,所以有很多重复的函数块...(偷懒

6981 Rise in Price

题意:

初始化宝石数量以及宝石价格为0。

给你一个宝石数量矩阵和宝石价格矩阵,每当你走到 i , j 位置时你可以拿走那个数字。

求最后你能获得的最大宝石总价值(即宝石数量×宝石价格)。

你只能从(1,1)位置走到(n,n)位置且每次只能往右走或者往下走。

思路:

如果只有宝石数量这个矩阵的话,那就是一个非常经典的二维DP了...

我们先分析只有一个矩阵的时候的情况...

很显然,当前状态(i , j)可以从(i - 1 , j)和(i , j - 1)转移而来

那么我们定义的状态转移函数就是:

其中f【i,j】表示的是走到当前下标(i , j)处我能获得的最大宝石数量...

emmmmmm....但是这个题,它多了一个状态,那就是还有钻石的价格需要考虑,于是....我们就可以多加入一个状态,然后再利用上面提到的状态转移方程...

由于保存钻石数量和钻石价格的两个状态是一致的...现在我们只分析保存钻石数量这个状态的状态转移怎么做

"笑死,多加一维不就成了!"

确实,不过看一眼数据,至少是1e6,得了,需要优化...

不过还是给出这个状态转移方程:

这里F【i,j,k】表示的是当我处在(i , j)位置时,拥有宝石数量为k + a[i][j]时,宝石价值的最大值

那么怎么优化呢?我们会发现,这个k其实不是每一个位置的状态有的,也即1 - MAX(能拿到的最多宝石数)这些状态有些是取不到的

那么对于每一个(i , j)我们只用保存能够得到的状态即可....因此这里的f[i,j]不是使用三维数组表示,而是使用表(vector or 链表)来进行存储...

我们再仔细观察...对于状态(i,j,k1)(i,j,k2)若k1 < k2 且这个状态(i,j,k1)<(i,j,k2)

那么k1这个状态就是最差劲的,因为你不仅宝石数量少,而且价格还低(QAQ

所以这是一个无效状态...

总归:

①每次状态转移时我们只需要枚举前两个状态其中的有效状态

②对于当前得到的所有状态(i,j)我们需要将“差劲状态”剔除,"差劲状态"满足k1 < k2 && (i,j,k1)<(i,j,k2)

因此,既然i,j是一个表,何不按照k的递增顺序进行存储呢,这样对于每次进来的一个状态 k'

我们能够保证k'一定大于k ,下面只需要判断(i,j,k1)<(i,j,k2)即可

如果k'这个状态的(i,j,k')大于前面的(i,j,k),就可以将(i,j,k)直接删除~

这里可能有点绕,我们讲的明白一些:

就是这样 ->

第一件事,我们假设

其中存储的都是有效状态,均是按照k值的大小单调递增的

第二件事,我们转移至(i,j)状态时,由于前两个状态都是递增有序的,那么我们其实只需要在O(N)的时间内合并两个数组即可!

由于a[i][j](宝石数量)和b[i][j](宝石价值)两个数组只会影响当前(i,j)状态,于是合并时并不需要考虑a和b带来的影响

合并操作:

如果k1 < k2那么我们肯定优先考虑将k1加入当前(i,j)状态集

①如果当前(i,j)状态集没有状态...我们直接加入

②否则将状态集最后那个状态的(i,j,k')与当前(i - 1,j,k1)进行比较,如果小于它的话,将(i,j,k')删除(注意!这是一个循环的操作

因为当前(i,j)状态集其中的k'一定是小于k1的!

最后将得到的所有状态集中k加上a[i][j]以及(i ,j,k)加上b[i][j]即可得到当前(i,j)所有有效状态~

代码:

 

点击查看代码
#include <iostream>
#include <algorithm>
#include <vector>
#pragma GCC opetimize(2)
using namespace std;
const int MAXN = 107;
typedef long long ll;
struct node{
    int k;//宝石数 
    int w;//权值(卖出的最大价格) 
    node(int k = 0,int w = 0):k(k),w(w){}
    bool operator < (const node &a)    const
    {return k < a.k;}
};
vector<node> dp[MAXN][MAXN];
int a[MAXN][MAXN];
int b[MAXN][MAXN];
/*
2
4
2 3 1 5
6 3 2 4
3 5 1 4
5 2 4 1
3 2 5 1
2 4 3 5
1 2 3 4
4 3 5 3

4
2 3 1 5
6 3 2 4
3 5 1 4
5 2 4 1
3 2 5 1
2 4 3 5
1 2 3 4
4 3 5 3
*/
int main()
{
//    freopen("1009.in","r",stdin);
//    freopen("ans.out","w",stdout);
    int t;
    scanf("%d",&t);
    dp[0][1].push_back(node(0,0));
    dp[1][0].push_back(node(0,0));
    while(t--)
    {
        int n;
        scanf("%d",&n);
        
        for(int i = 1;i <= n;++i)
        for(int j = 1;j <= n;++j)
        scanf("%d",&a[i][j]);
        
        for(int i = 1;i <= n;++i)
        for(int j = 1;j <= n;++j)
        scanf("%d",&b[i][j]);

        for(int i = 1;i <= n;++i)
        for(int j = 1;j <= n;++j)
        {
            dp[i][j].clear();
            int k1 = dp[i - 1][j].size();
            int k2 = dp[i][j - 1].size();
            int i1 = 0,i2 = 0;
            while(i1 < k1 && i2 < k2)
            {
                while(i1 < k1 && dp[i - 1][j][i1].k < dp[i][j - 1][i2].k)
                {
                    while(dp[i][j].size() && dp[i][j].back().w <= dp[i - 1][j][i1].w)
                    dp[i][j].pop_back();
                    
                    dp[i][j].push_back(dp[i - 1][j][i1]);
                    ++i1;
                }
                
                while(i2 < k2 && dp[i][j - 1][i2].k <= dp[i - 1][j][i1].k)
                {
                    while(dp[i][j].size() && dp[i][j].back().w <= dp[i][j - 1][i2].w)
                    dp[i][j].pop_back();
                    
                    dp[i][j].push_back(dp[i][j - 1][i2]);
                    ++i2;
                }
            }
            while(i1 < k1)
            {
                while(dp[i][j].size() && dp[i][j].back().w <= dp[i - 1][j][i1].w)
                dp[i][j].pop_back();
                
                dp[i][j].push_back(dp[i - 1][j][i1]);
                ++i1;
            }
            while(i2 < k2)
            {
                while(dp[i][j].size() && dp[i][j].back().w <= dp[i][j - 1][i2].w)
                dp[i][j].pop_back();
                
                dp[i][j].push_back(dp[i][j - 1][i2]);
                ++i2;
            }
            for(int k = 0;k < dp[i][j].size();k++)
            {
                dp[i][j][k].w += b[i][j];
                dp[i][j][k].k += a[i][j];
            }
//            for(int k = 0;k < dp[i][j].size();k++)
//            printf(">>%d %d -> %d %d\n",i,j,dp[i][j][k].k,dp[i][j][k].w);
            //将i,j中所有 k1 < k2 && w1 < w2的状态删除 
        }
        ll ans = 0;
        for(int i = 0;i < dp[n][n].size();i++)
        ans = max(ans,1ll*dp[n][n][i].k*dp[n][n][i].w);
        printf("%lld\n",ans);
    }
    return 0;
}

6982 Road Discount

题意:

给定一些边,其中一条边有两个权值,一个是初始权值,一个是折扣权值。

问:当我可以任意取K∈[0,n]条折扣权值时,其构成最小生成树权值大小是多少?

思路:

QAQ当时没有细想这个题,现在只能面向题解编程了,可能有些地方理解不透彻,还希望各位dalao指正~

第一件事:

如果我们K = 0,这时候构成一颗最小生成树就是用所有初始权值来构造

如果我们K = n,这时候构成一颗最小生成树就是用所有折扣权值来构造

考虑K ∈ [1 , n - 1],思考这样一个问题:现在的K条件下构成最小生成树的边是否存在以上两种特殊情况之外的边呢?

答案肯定的,我们简单证明一下:

假设K = 0时构成最小生成树的边集是e1,K = n时是e2

如果K ∈ [1 , n - 1],其中构成最小生成树的边e存在e ∉ {e1 , e2};

分两种情况:

①这条边是初始权值

②这条边是折扣权值

如果是①,那么显然选择e这条边的会使得我目前的答案(K ∈ [1 , n - 1])更优,那么这条边一定会存在于e1当中!如果不在的话,那e1中的边不就不是最优的了!

显然,对于第二种情况e这条边一定存在于e2这条边中!

因此假设不成立。

K ∈ [1 , n - 1]时所有边都存在于 {e1 , e2}中。

于是我们可以在O(MlogN)时间内求出我们需要的所有边,然后我们来思考这样一个问题:

如何在现在的{e1,e2}集合里面挑出K个e2的边,挑出N - K个e1的边?

题解的方法是,将所有的折扣边都加上一个给定的值,这样的话我在构成最小生成树的时候就能产生不同的选择(到底是选择折扣边还是初始权值呢?)

QAQ到这里我就无能为力了,可能是一种是一种常用方法吧,证明我也不会,如果有daolao会能不能教教我

由于边权大小差最多不会超过1000,于是我们只需要枚举[0,1000]所有的可能的给定的值就好了,然后求1001次该状态下的最小生成树

所有加上c固定边权的最小生成树我们需要保存以下状态:

①此时我们拿走的折扣边数量cnt

②此时的最小生成树的权值和sum

设c = [0,1000]这个固定的值,很容易就能想到,当c递增时,所选的cnt是非递增的(因为这样折扣边 + c的值会越来越大,直到大于原始边权,就不会再拿折扣边了

于是对于每一个询问的K值,我们可以二分(暴力)去离线回答询问,在所有的c ∈ [0,1000]数组中寻找第一个小于等于K的cnt值,这样,所取的下标c值就是当前加上的固定边权

然后sum - c*k就是答案~

代码

点击查看代码
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1005;
const int MAXM = 200005;
struct node
{
    int x,y;
    int w;
}a[MAXM],b[MAXM];
int fa[MAXN];
int n,m;
void ini()
{for(int i = 0;i <= n;i++)    fa[i] = i;}
int Find(int x)
{
    if(x == fa[x])    return x;
    return fa[x] = Find(fa[x]);
}
bool merge(int x,int y)
{
    x = Find(x);
    y = Find(y);
    if(x != y)
    {
        fa[x] = y;
        return true;
    }
    return false;
}
bool cmp(node a,node b)
{return a.w < b.w;}
void Minus(node *a)
{//将初始的最小生成树建立号,这时候的边就是以后会选择的最好的边 
    ini();
    sort(a,a + m,cmp);
    for(int i = 0,cnt = 0;i < m;i++)
    if(merge(a[i].x,a[i].y))    a[cnt++] = a[i];
}
struct NO
{
    int sum;//权值总和 
    int cnt;//黑边数量 
    NO(int sum = 0,int cnt = 0):sum(sum),cnt(cnt){}
}all[MAXN];
NO go(int c)
{//每条边添加权值c,此时最多能拿多少条黑边呢(最小生成树中)? 
    ini();
    int cnt = 0;//黑边数量
    int num = 0;//最小生成树中节点个数,其实没啥用,因为一定可以构成一颗最小生成树 
    int k1 = 0,k2 = 0;
    
    int sum = 0;
    while(k1 < n - 1 && k2 < n - 1)
    {//至多选择n - 1条边 
        if(a[k1].w <= b[k2].w + c)
        {//选择白边更优 
            if(merge(a[k1].x,a[k1].y))
            sum += a[k1].w,num += 1;
            ++k1; 
        }
        else
        {//选择黑边加上一个权值还是最优的 
            if(merge(b[k2].x,b[k2].y))
            sum += b[k2].w + c,++cnt,num += 1;
            ++k2;
        }
        if(num == n - 1)
        break;
    }
    while(k1 < n - 1)
    {
        if(merge(a[k1].x,a[k1].y))
        sum += a[k1].w;
        ++k1;
    }
    
    while(k2 < n - 1)
    {
        if(merge(b[k2].x,b[k2].y))
        sum += b[k2].w + c,++cnt;
        ++k2;
    }
    return NO(sum,cnt);
}
int ask(int x)
{//由于all数组是权值越来越大的,那么它的黑边数也会越来越少,因此找到第一个 
//黑边数符合 <= x的那个权值,就是答案
    for(int i = 0;i <= 1000;i++)//减去多加的那一部分 
    if(all[i].cnt <= x)    return all[i].sum - x*i;
    return -1;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d %d",&n,&m);
        for(int i = 0;i < m;i++)
        {
            scanf("%d %d %d %d",&a[i].x,&a[i].y,&a[i].w,&b[i].w);
            b[i].x = a[i].x;
            b[i].y = a[i].y;
        }
        
        Minus(a);
        Minus(b);
        for(int i = 0;i <= 1000;i++)
        all[i] = go(i);//预处理当所有黑边加上一个数i后所构成的最小生成树的黑边数量 
        
        for(int i = 0;i < n;i++)
        {//拿去i条黑边时构成的最小生成树的权值和 
            printf("%d\n",ask(i)); 
        }
    }
    return 0;
}

待补知识点:

FFT、熟练泼粪(树链剖分) +平衡树、最小独立集

唠一嘴:FHQ Treap真的太厉害了!

posted @ 2021-07-28 20:29  K0njac  阅读(227)  评论(1编辑  收藏  举报