首届CCF算法能力大赛(CACC)区域赛回忆版题解
觉得这次题出的都还不错,于是记录下。
一、报数游戏
问题: 一群同学围着一圈坐,编号 1 1 1 到 n n n ,给定 m m m ,从第一个同学开始报数开始报 m m m 个后,第 m m m 位同学离场,从圆圈中离开,剩余同学继续每隔 m m m 个报数离场一个人,问最后剩余的那个同学编号是多少。
答案: 经典的约瑟夫环板子,甚至一点没改。不过数据范围才 1000 1000 1000,暴力模拟也能过。代码用约瑟夫环板子写。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n,m,res = 0;
cin>>n>>m;
for (int i = 1; i <= n; ++i)
res = (res + m) % i;
cout << res+1<< endl;//+1表示下标从1开始
return 0;
}
二、宝藏勘探
问题: 在一个从 0 0 0 到 n n n 的数轴上,每个位置有一个宝藏辐射值。两位勘探员正在勘探宝藏,勘探过的位置中的最大值和最小值之差代表整块数轴地的辐射值,能反映这块地方的宝藏可能性。两位勘探员分别从数轴的两侧相向而行,但是他们之间有矛盾,不愿意隔得太近,两位勘探员中间只剩 k k k 个单位长度时,就会停止前进。目前不知道两位勘探员谁先出发,只知道他们各自从两端相向而行向中间靠拢,中间相隔 k k k 个位置的时候停止勘探。问这块数轴地可能最小的宝藏辐射值是多少?
答案: 经典的单调队列问题,这题解法有很多种,比如优先队列+延迟删除(力扣例题)、RMQ等等。由于这题一定是去掉中间的值,停止勘探区域移动过程中,可以将数组分为前部分和后部分,前部分可以边移动边更新前部分最大最小值,后部分则可以先按照前部分的计算方法倒着更新,将他们预处理然后更新答案即可。代码用单调队列写。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=100005;
const int INF=1000000009;
int a[MAXN],rmin[MAXN],rmax[MAXN],lmin[MAXN],lmax[MAXN];//rmin[i]表示第i个数及右边所有的数的最小值,lmax[i]表示第i个数及左边所有数的最大值
int main() {
int n,k,ans=INF;
cin>>n>>k;
for (int i = 1; i <= n; i++)
cin>>a[i];
lmin[0]=rmin[n+1]=INF;
for(int i=1; i<=n; i++) {
lmin[i]=min(lmin[i-1],a[i]);
lmax[i]=max(lmax[i-1],a[i]);
}
for(int i=n; i>=1; i--) {
rmin[i]=min(rmin[i+1],a[i]);
rmax[i]=max(rmax[i+1],a[i]);
}
for(int i=2; i<=n-k; i++)//注意从2开始,因为第一个和最后一个一定要算在内
ans=min(ans,max(lmax[i-1],rmax[i+k])-min(lmin[i-1],rmin[i+k]));
cout<<ans<<endl;
return 0;
}
三、心智能力
问题: 小明小红两个人进行记忆和心算能力测试,两个人各自脑海中有一个初始化为全零长度为
n
n
n 的数组,互相对对方的某个指定区间做加减修改,但是发现太容易了,于是加大难度。现在对对方的某个区间进行统一增加/减少修改时,需要指定对值为奇数还是偶数的值做操作(注意不是下标的奇偶性,而是值的奇偶性,当时做的时候就看岔了 ),小明觉得有点难度了,请你帮他设计一个程序完成小红的挑战。
简而言之:有一个长度为 n ( ≤ 2 e 5 ) n (\leq2e5) n(≤2e5) 的数组,对其进行 m ( ≤ 2 e 5 ) m(\leq2e5) m(≤2e5) 次的操作:
1 l r odd add
: 对下标在 [ l , r ] [l,r] [l,r] 且奇偶性为 o d d odd odd(1为奇数,0为偶数)的数加上 a d d add add2 l r
:查询下标 [ l , r ] [l,r] [l,r] 元素之和
答案: 典中典之带懒惰标记范围修改线段树板子,但是无法解决根据奇偶性相加的问题。
这题的痛点在于:“维护奇偶性变换则区间查询复杂度高,维护区间查询则奇偶性变换复杂度高”。
题主也想了很久,我们要在最多 o ( l o g 2 n ) o(log_2n) o(log2n) 的复杂度上解决奇偶变换问题,假如处理一个奇偶交替的区间,变换奇偶时间至少 o ( n ) o(n) o(n),且这会发生多次(比如一直在大范围的奇偶交替区间加偶数)。
所以,不能直接修改奇偶性,也需要Lzay数组记录奇偶,且根据多次模拟,能发现一个结论:假设某个线段树节点表示范围为 [ L , R ] [L,R] [L,R] ,对于满足 l i ≤ L l_i \leq L li≤L 且 R ≤ r i R \leq r_i R≤ri 的条件下的任意修改操作 1 1 1 l i l_i li r i r_i ri o d d i odd_i oddi a d d i add_i addi ,范围 [ L , R ] [L,R] [L,R] 要不所有数的奇偶性不变,要不只能从有奇有偶的情况“单向”变化到全奇或全偶的情况。
以上结论很难理解,我举个例子:你可以用若干次操作 1 1 1 1 1 1 3 3 3 o d d i odd_i oddi a d d i add_i addi 使数组 [ 1 , 0 , 1 ] [1,0,1] [1,0,1] 变成全偶或全奇,但无法用若干次操作 1 1 1 1 1 1 3 3 3 o d d i odd_i oddi a d d i add_i addi再让他变回 [ 1 , 0 , 1 ] [1,0,1] [1,0,1]。于是,在还没有pushdown的线段树节点中,我们可以画个状态图:
(猜你会问:“
[
1
,
1
,
1
]
[1,1,1]
[1,1,1]我用操作 1 2 2 1 -1
不就变回
[
1
,
0
,
1
]
[1,0,1]
[1,0,1] 了吗?”,但是因为范围
[
2
,
2
]
[2,2]
[2,2] 是节点
[
1
,
3
]
[1,3]
[1,3] 的子树节点,pushdown时会把问题交给子树处理,就不用考虑啦~)
我们先写好写的第二阶段全奇全偶区域,赋予普通的 l z lz lz 的懒惰标记,就有:
- 如果是全奇数且要加奇数: l z [ r t ] + = a d d , t r e e [ r t ] + = l e n ∗ a d d lz[rt]+=add,tree[rt]+=len*add lz[rt]+=add,tree[rt]+=len∗add ( l e n len len为节点表示的数组长度)
- 如果是全奇数且要加偶数: r e t r u n retrun retrun
- 如果是全偶数且要加奇数: r e t r u n retrun retrun
- 如果是全偶数且要加偶数: l z [ r t ] + = a d d , t r e e [ r t ] + = l e n ∗ a d d lz[rt]+=add,tree[rt]+=len*add lz[rt]+=add,tree[rt]+=len∗add
再写第一阶段,由于所有的奇偶性不会变,所以我们可以开两个懒惰标记, o l z [ i ] olz[i] olz[i] 表示 i i i 表示的范围中的所有奇数要加的值, e l z [ i ] elz[i] elz[i] 表示i表示的范围中的所有偶数要加的值:
- 如果要加奇数: o l z [ r t ] + = a d d , t r e e [ r t ] + = n u m [ r t ] ∗ a d d olz[rt]+=add,tree[rt]+=num[rt]*add olz[rt]+=add,tree[rt]+=num[rt]∗add( n u m [ r t ] num[rt] num[rt]为该节点表示范围的奇数数量)
- 如果要加偶数: e l z [ r t ] + = a d d , t r e e [ r t ] + = ( l e n − n u m [ r t ] ) ∗ a d d elz[rt]+=add,tree[rt]+=(len-num[rt])*add elz[rt]+=add,tree[rt]+=(len−num[rt])∗add
剩下的就是维护num值和修改push_down了,全是细节,代码上都有备注,需要注意的是,题主代码从以下方面节省了存储空间防止MLE:
- 根据结论,数组只能从原始状态变为全0或全1,可以在push_down中判定父节点的num是否为0或者等于长度,如果是则再更新num,否则一定没更新num。
- 根据结论,当第二阶段时一定是加范围全部元素,加在 l z [ r t ] lz[rt] lz[rt] 上等价于加在 o l z [ r t ] olz[rt] olz[rt] 和 e l z [ r t ] elz[rt] elz[rt] 上。
终于复杂度降到 o ( m l o g 2 n ) o(mlog_2n) o(mlog2n)了,不容易啊,想了我半天做出来的QAQ。
#include<bits/stdc++.h>
#define js ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define int long long
using namespace std;
const int MAXN= 200005;
int n;//因为数组大小需要线段树也知道所以放在外面
int tree[4*MAXN];
int olz[4*MAXN];//偶数lazy,old[i]表示i表示的范围中的所有奇数要加的值
int elz[4*MAXN];//奇数lazy,eld[i]表示i表示的范围中的所有偶数要加的值
int num[4*MAXN];//奇数个数,num[i]表示i表示的范围中奇数个数(偶数=长度-num[i])
void push_down(int l,int r,int rt) {
if(elz[rt]||olz[rt]) {
int mid = (l+r) / 2;
olz[rt<<1] += olz[rt];//类似lz[rt<<1]+=lz[rt]原理
olz[rt<<1|1] += olz[rt];
elz[rt<<1] += elz[rt];
elz[rt<<1|1] += elz[rt];
tree[rt<<1] += num[rt<<1]*olz[rt]+(mid - l + 1-num[rt<<1])*elz[rt];//奇数lazy乘奇数的个数,偶数lazy乘偶数的个数
tree[rt<<1|1] += num[rt<<1|1]*olz[rt]+(r - mid-num[rt<<1|1])*elz[rt];
olz[rt] =elz[rt]= 0;//类似lz[rt]=0原理
if(num[rt]==0)//如果他父亲已变为全偶,则他的两个孩子全偶
num[rt<<1]=num[rt<<1|1]=0;
else if(num[rt]==r-l+1) {//如果他父亲已变为全奇,则他的两个孩子全奇
num[rt<<1]=mid - l + 1;
num[rt<<1|1]=r - mid;
}//否则,必定奇偶没变
}
}
void update_range(int l,int r,int add,int L,int R,int rt,int isodd) {
if(l <= L && r >= R) {
if(num[rt]==0||num[rt]==R-L+1) {//全奇或全偶情况
if(isodd&&num[rt]==R-L+1||(!isodd)&&num[rt]==0) {//全奇情况且加在奇数上或全偶情况且加在偶数上
olz[rt]+=add;
elz[rt]+=add;
tree[rt]+=(R-L+1)*add;
}//否则一定是全奇情况且加在偶数上或全偶情况且加在奇数上,此操作相当于没加
} else {//有奇有偶情况
if(isodd) {//加在奇数上
olz[rt]+=add;
tree[rt]+=num[rt]*add;//奇数lazy乘奇数的个数
} else {//加在偶数上
elz[rt]+=add;
tree[rt]+=(R-L+1-num[rt])*add;//偶数lazy乘偶数的个数
}
}
if(add&1) { //加奇数改变奇偶性
if(isodd)//奇+奇=偶
num[rt]=0;
else//偶+奇=奇
num[rt]=R-L+1;
}
return;
}
push_down(L,R,rt);
int mid = (L+R) / 2;
if(mid >= l) update_range(l,r,add,L,mid,rt<<1,isodd);
if(mid < r) update_range(l,r,add,mid+1,R,rt<<1|1,isodd);
tree[rt] = tree[rt<<1] + tree[rt<<1|1];
num[rt] = num[rt<<1] + num[rt<<1|1];//num值也要向上更新
}
int query_range(int l,int r,int L,int R,int rt) {
if(l <= L && r >= R) return tree[rt];
push_down(L,R,rt);
int mid = (L+R) / 2;
long long tsum = 0;
if(mid >= l) tsum += query_range(l,r,L,mid,rt<<1);
if(mid < r) tsum += query_range(l,r,mid+1,R,rt<<1|1);
return tsum;
}
signed main() {
js;//cin关流,为了提高读写速度,跟本题思路无关
int m;
cin>>n>>m;
while(m--) {
int op;
cin>>op;
if(op==1) {
int l,r,isodd,add;
cin>>l>>r>>isodd>>add;
update_range(l,r,add,1,n,1,isodd);
} else {
int l,r;
cin>>l>>r;
cout<<query_range(l,r,1,n,1)<<endl;
}
}
return 0;
}
四、牌型分组
问题: 给定一副扑克牌,牌型分组的时候无需考虑花色。普通扑克牌一组 13 张,这里可以拓展为更普适的情况,设扑克牌一组 n ( ≤ 2000 ) n(\leq2000) n(≤2000) 张。扑克牌的每张牌有牌点,点数最大的是大王,然后是小王 , 2 , A (以下简称头 A ) , n , n − 1 … 4 , 3 ,2,A(以下简称头A),n,n-1…4,3 ,2,A(以下简称头A),n,n−1…4,3,可以考虑的牌型如下:
- 单张
- 对子(牌点相同的两张牌)
- 三张(牌点相同的三张牌)
- 顺子(点数顺序的五张牌,如 [ 10 , 9 , 8 , 7 , 6 ] [ 10 , 9 , 8 , 7 , 6 ] [10,9,8,7,6] )
其中对于顺子来说有一些特殊要求:
- A在作为顺子的时候可以作为数字1(以下简称尾A),当 [ 5 , 4 , 3 , 2 , A ] [ 5 , 4 , 3 , 2 , A ] [5,4,3,2,A] 打出。
- 不能出现 [ … , 2 , A , n,… ] 的顺子,也就出顺子牌的时候 2 2 2 不能作为 A A A 的下一张
- 大小王不能用顺子带
要求是使用上述牌组打出,使得最后剩余的牌点严格比 A A A 小的单张牌数量最少,并输出数量(以下简称答案数)。先给出 n n n 和样例数 m ( ≤ 10 ) m(\leq 10) m(≤10),每行字符串表示一组牌组,比如 ( H A H 13 H 12 H 11 ) (HA H13 H12 H11) (HAH13H12H11)表示红桃A、红桃K、红桃Q和红桃J,每个用例的牌不超过 1 e 5 1e5 1e5。
答案: 一眼二进制枚举 铁T!是贪心 WA了!动态规划!(QAQ好难)
先不考虑 A A A(因为他即可变n+1又能变1),不难得出:
- 结论① 如果一种牌有两张及以上,则可以通过对子和三张打掉,只有一张的时候,只能用顺子带。
- 结论② 如果五张点数连续的牌每种都有两张的时候,其当两组顺子的效果无异于打出五组对子,三组及以上顺子同理。
因为结论①,所以可以设动态规划 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示到点数为 i i i牌时的答案数,其中 i i i 表示点数 i i i 及更大的数, j = 1 j=1 j=1表示打出 [ i + 4 , i + 3 , i + 2 , i + 1 , i ] [i+4,i+3,i+2,i+1,i] [i+4,i+3,i+2,i+1,i]的顺子, j = 0 j=0 j=0表示不打出该顺子,(因为结论②,所以可以简化 j > 1 j>1 j>1的情况)
但是你会发现这样不能建立的动态规划公式,因为若要对点数为 [ i + 4 , i ] [i+4,i] [i+4,i] 二进制枚举顺子,而这会影响点数为 [ i + 8 , i ] [i+8,i] [i+8,i] 的数量, d p dp dp的值就不能用了。
因为要枚举前五位,不妨将五位都作为状态量,记为 d p [ i ] [ j 4 ] [ j 3 ] [ j 2 ] [ j 1 ] [ j 0 ] dp[i][j_4][j_3][j_2][j_1][j_0] dp[i][j4][j3][j2][j1][j0],其中 j k = 1 j_k=1 jk=1表示打出 [ i + k + 4 , i + k + 3 , i + k + 2 , i + k + 1 , i + k ] [i+k+4,i+k+3,i+k+2,i+k+1,i+k] [i+k+4,i+k+3,i+k+2,i+k+1,i+k]的顺子,为方便起见,可以整合后用二进制表示,即 d p [ i ] [ S ] dp[i][S] dp[i][S],比如 S S S 为5个 j k j_k jk组合的二进制数, S = 10 = ( 01010 ) 2 S=10=(01010)_2 S=10=(01010)2表示打出 [ i + 8 , i + 7 , i + 6 , i + 5 , i + 4 ] [i+8,i+7,i+6,i+5,i+4] [i+8,i+7,i+6,i+5,i+4]和 [ i + 5 , i + 4 , i + 3 , i + 2 , i + 1 ] [i+5,i+4,i+3,i+2,i+1] [i+5,i+4,i+3,i+2,i+1]的顺子。
之后就可以如 d p [ i ] [ S ] = d p [ i − 1 ] [ S ′ ] dp[i][S]=dp[i-1][S'] dp[i][S]=dp[i−1][S′] 的形式只管上一层了,然后你就可以发现打出 [ i + 6 , i + 5 , i + 4 , i + 3 , i + 2 ] [i+6,i+5,i+4,i+3,i+2] [i+6,i+5,i+4,i+3,i+2] 的顺子可以用 d p [ i ] [ ( ? ? ? 1 ? ) 2 ] dp[i][(???1?)_2] dp[i][(???1?)2] 和 d p [ i − 1 ] [ ( ? ? ? ? 1 ) 2 ] dp[i-1][(????1)_2] dp[i−1][(????1)2] 表示,所以可以拓展到 d p [ i ] [ ( 1010 ? ) 2 ] = d p [ i − 1 ] [ ( ? 1010 ) 2 ] dp[i][(1010?)_2]=dp[i-1][(?1010)_2] dp[i][(1010?)2]=dp[i−1][(?1010)2] ,化作十进制 S S S 则是除以2,则状态转移方程如下:
设当前点数为 i i i 为 s u m sum sum , S S S 含1的数量为 n u m 1 num1 num1:
- 当 s u m − n u m 1 = = 1 sum-num1==1 sum−num1==1 时, d p [ i ] [ S ] = m i n ( d p [ i − 1 ] [ S / 2 ] , d p [ i − 1 ] [ S / 2 + 16 ] ) + 1 dp[i][S]=min(dp[i-1][S/2],dp[i-1][S/2+16])+1 dp[i][S]=min(dp[i−1][S/2],dp[i−1][S/2+16])+1
- 当 s u m − n u m 1 < 0 sum-num1<0 sum−num1<0 时, d p [ i ] [ S ] = I N F dp[i][S]=INF dp[i][S]=INF
- 否则, d p [ i ] [ S ] = m i n ( d p [ i − 1 ] [ S / 2 ] , d p [ i − 1 ] [ S / 2 + 16 ] ) dp[i][S]=min(dp[i-1][S/2],dp[i-1][S/2+16]) dp[i][S]=min(dp[i−1][S/2],dp[i−1][S/2+16])
最后再考虑 A A A ,由于动态态规划无后向性,所以需要讨论有多少头 A A A 多少尾 A A A,分类讨论改下统计数组多dp几遍就行,因为结论②,作为带头的 A A A 以及尾巴的 A A A,最多只会有2张 A A A作为顺子带出,即当有两张及以上 A A A时,最多只要dp2次情况即可:先将 A A A作为头 A A A,dp一次,再将一个头 A A A变成尾 A A A,再dp一次,两者取最小值即可。
复杂的为 o ( 2 ∗ 2 5 n m ) o(2*2^5nm) o(2∗25nm), n n n 为牌数, m m m为用例数。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=100005;
const int INF=1000000009;
int dp[MAXN][32];
map<int,int>mp;
int pow2[]={1,2,4,8,16,32};//pow2[i]表示2的i次方
int dpres(int n) {
for(int j=0;j<32;j++)//设初始值
dp[n+2][j]=0;
for(int i=n+1;i>=1;i--)
for(int j=0;j<32;j++){
int ji=j,num1=0;//统计j有多少个一,来计算当前种类牌被顺了多少个
while(ji){
if(ji&1)
num1++;
ji/=2;
}
if(mp[i]-num1<0||j>=pow2[min(n+2-i,5)])//非法情况:当前种类牌不够、前面的种类不存在
dp[i][j]=INF;
else if(i!=n+1&&i!=2&&i!=1&&mp[i]-num1==1)//顺带完只有一张的情况(A、2不会计算在答案中)
dp[i][j]=min(dp[i+1][j/2],dp[i+1][j/2+16])+1;
else//能完全顺带完的情况
dp[i][j]=min(dp[i+1][j/2],dp[i+1][j/2+16]);
}
return min(dp[1][0],dp[1][16]);//只有不成顺子或者[5,4,3,2,1]是合法的
}
int main() {
int n,m;
cin>>n>>m;
string s;
getchar();
while(getline(cin,s)) {
mp.clear();
int num=0;
s+=' ';//按空格来处理数字,所以多加一个处理最后一个数字
for(int i=0; i<s.size(); i++)
if(s[i]==' ') {
if(num)
mp[num]++;
num=0;
} else if(s[i]=='A')//如果是A,先作为n+1计算
num=n+1;
else if(s[i]>='0'&&s[i]<='9')
num=(int)(s[i]-'0')+num*10;
int ans=dpres(n);
if(mp[n+1]){//如果存在A,则可以试试放一张做尾A。
mp[n+1]--;
mp[1]++;
ans=min(ans,dpres(n));
}
cout<<ans<<endl;
}
return 0;
}
五、资源分配(可能正确)
问题: 初始给定一组逻辑处理器数量和物理内存固定的物理机,现在要处理客户创建虚拟机的资源分配需求,要求实现:
init(n, c, m)
表示初始化 n n n 台具有 c c c 个逻辑处理器以及 m m m GB 物理内存的物理机。无需返回任何值。create(vid, vcpu, vmem)
表示需要在某个物理机上创建一个虚拟机ID为 vid 的,需要 vcpu 个逻辑处理器以及 vmem GB 物理内存的虚拟机,如果资源不够(没有任何一台物理机的剩余量能够同时满足 cpu 和 mem 的需求),可以申请立刻扩容一台物理机再分配(所有物理机规格和初始化时候的机器配置一样),返回值是一个二元组,表示虚拟机实际被分配到的物理机编号和是否扩容了一台物理机(如被分配在1号物理机,则返回 {1,0},如果分配在新增的2号物理机,返回 {2,1})。题目保证创建的虚拟机使用的cpu和内存都不大于物理机。remove(vid)
,删除虚拟机ID为 vid 的虚拟机,将计算资源归还给对应的物理机。无需返回任何值。
给出的输入不存在非法请求,最多 create(vid, vcpu, vmem)
50000次,最多存在10000物理机,执行目的是请使用尽可能少的物理机完成每个测试用例的虚拟机分配创建请求。
提供的文件:
- 20个输入样例,输入样例生成数据,生成的评判样例与该20样例服从同一分布。
Scheduler.cpp
,包含上述需要实现的三个函数,需要完成后提交。main.cpp
,内测评判文件,写入Scheduler.cpp
文件后给出评分。
评分标准:main.cpp
给定一个已经实现的虚拟机分配算法作为基线算法(执行create(vid, vcpu, vmem)
时放置在最小编号的能放置的物理机上),设总共有
n
n
n个评测样例,运行第
i
i
i个样例时基线代码使用的物理机数量为
N
i
N_i
Ni,你的Scheduler.cpp
使用的物理机数量为
M
i
M_i
Mi ,则你的得分为
m
i
n
(
200
∗
0.6
∗
1
n
∑
i
=
1
n
N
i
M
i
,
200
)
min(200*0.6*\frac{1}{n}\sum_{i=1}^n\frac{N_i}{M_i},200)
min(200∗0.6∗n1∑i=1nMiNi,200)。
答案: 题主用的是动态分区分配算法的最佳适应算法,因为不知道要创建多少台虚拟机,所以最优的办法就是:能放下就放,放不下再开。
所以现在问题是在能放下的机子中选择哪一台创建。由于有两个变量,所给的数据是均匀分布,题主根据计算创建该虚拟机后的损失的虚拟机种类值 v a l = v [ i ] . f i r s t ∗ v [ i ] . s e c o n d − ( v [ i ] . f i r s t − v c p u ) ∗ ( v [ i ] . s e c o n d − v m e m ) val=v[i].first*v[i].second-(v[i].first-vcpu)*(v[i].second-vmem) val=v[i].first∗v[i].second−(v[i].first−vcpu)∗(v[i].second−vmem),并选择最小值来进行创建。
例如,有一台剩2个gpu、2GB的物理机,和一台剩1个gpu、4GB的物理机,我现在要创建一个1个cpu,1GB的虚拟机,因为第一台物理机可以创建 2×2=4种(1cpu1GB、1cpu2GB、2cpu1GB、2cpu2GB)、第二台物理机可以创建4种,如果在第一台物理机创建后,变成1cpu1GB了,只能创建1种,损失了3种,而在第二台物理机创建后则不能创建(0×3=0),损失4种,我每次选择创建后种类损失最小的物理机来创建虚拟机,即第一台。代码如下:
#include<bits/stdc++.h>
using namespace std;
struct node {
int N,C,M;
vector<pair<int,int>>v;//当前物理机数组,v.first为剩余cpu量,v.second为剩余内存
map<int,pair<int,pair<int,int>>>mp;//当前虚拟机映射,mp.first为所在物理机id,mp.second.first为所用cpu,mp.second.second为所用内存
void init(int n,int c,int m) {
N=n;
C=c;
M=m;
for(int i=0; i<n; i++)
v.push_back({c,m});
}
pair<int,int> create(int vid,int vcpu,int vmem) {
int id=-1,op=0;
int minn=1000000009;
for(int i=0; i<v.size(); i++)
if(v[i].first>=vcpu&&v[i].second>=vmem) {
int val=v[i].first*v[i].second-(v[i].first-vcpu)*(v[i].second-vmem);//关键代码
if(val<minn) {
minn=val;
id=i;
}
}
if(id!=-1) {//没找到合适的再新开
id=N++;
op=1;
v.push_back({C,M});
}
mp[vid]= {id,{vcpu,vmem}};//记录,使remove操作能o(1)解决
v[id].first-=vcpu;
v[id].second-=vmem;
return {id,op};
}
void remove(int vid) {
v[mp[vid].first].first+=mp[vid].second.first;
v[mp[vid].first].second+=mp[vid].second.second;
}
};
如有更好的算法,欢迎在评论区讨论~
感谢 sightONlast 提供题面