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真的太厉害了!