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