题解 CF1348 D,E,F Codeforces Round #638 (Div. 2)
CF1348D Phoenix and Science
分析问题的性质。首先,每块的大小是无关紧要的,重要的是你每天会切多少块。这取决于两个因素:当前已有的块数\(d\),当前所有块的总和\(w\)。假设你今天要新切\(x\)刀,那么显然,\(i\)必须满足:\(0\leq x\leq d\)。切完后,块数会变为\(d+x\)。在今天夜里过后,重量会变为\(w+d+x\)。那么显然,我们要求\(w+d+x\leq n\)。也就是说,\(0\leq x\leq \min(d,n-w-d)\)。这就是我们一天中能做的事。
为了使天数最少,在最开始的阶段,我们肯定希望\(w\)快速增长。因此我们每天都令\(x=\min(d,n-w-d)\)。直到某一天,\(n-w-d<0\)时,这时候无法再进行任何的操作了。如果此时刚好\(w=n\),那就是圆满的结果。但是在更多的情况下,此时的\(w\)可能会比\(n\)差一点点。我们要想办法补上这“一点点”。
从后往前,枚举之前的每一次操作。设相邻两次操作后的块数分别为\(d_1\), \(d_2\),那么我们可以在\(d_1\), \(d_2\)之间插入一次操作,要满足在这次操作之后的块数\(D\):\(d_1\leq D\leq d_2\)。同时,\(D\)要尽可能大,所以令\(D=\min(d_2,n-w)\)即可(若\(\min(d_2,n-w)<d_1\),说明在\(d_1\), \(d_2\)之间插入不可行,我们直接从后往前枚举更小的操作即可)。如此从后往前,能插就插,显然,这样的贪心能够使总操作次数最少。且最坏的情况下,也一定能凑到\(n\),因为最前面可以做无限次\(+1\)的操作。
实测,当\(n=10^9\)时,只需要\(29\)次操作。故时间复杂度不超过\(O(29n)\)。
参考代码:
//problem:CF1348D
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
int main() {
int T;cin>>T;while(T--){
ll n;cin>>n;
ll d=1;
ll w=1;
vector<pair<ll,ll> > ans;
while(true){
ll i=min(d+d,n-w);
if(i>=d){
w+=i;
ans.pb(mk(i-d,i));
d=i;
}
else break;
}
for(int i=SZ(ans)-2;i>=-1;--i){
for(ll j=min(n-w,ans[i+1].se);j>=(i==-1?1:ans[i].se);--j,j=min(j,n-w)){
w+=j;
ans[i+1]=mk(ans[i+1].se-j,ans[i+1].se);
ans.insert(ans.begin()+i+1,mk(j-(i==-1?1:ans[i].se),j));
++i;++j;
if(w==n)break;
}
if(w==n)break;
}
cout<<SZ(ans)<<endl;
for(int i=0;i<SZ(ans);++i){
cout<<ans[i].fi<<" ";
}
cout<<endl;
}
return 0;
}
CF1348E Phoenix and Berries
我们称两种放到篮子里的方式分别为:“同一棵树”,“同一品种”。
我们发现,一定存在一种最优方案,使得每棵树上的红莓或蓝莓作为“同一品种”被放入篮子的数量均不超过\(k-1\)个。因为如果一棵树上红莓或蓝莓存在大于等于\(k\)个“同一品种”,则完全可以把它们这篮换成“同一棵树”。于是可以想到一种朴素的DP。设\(dp[i][x][y]\)表示考虑了前\(i\)棵树,红莓“同一品种”的数量\(\bmod k\)余\(x\)个,蓝莓“同一品种”的数量\(\bmod k\)余\(y\)个,最多放满了多少个篮子。转移时枚举当前树上“同一品种”的红、蓝莓各有多少个,其他全部作为“同一棵树”。时间复杂度\(O(nk^4)\)。
考虑优化。一个想法是只枚举选多少红莓作为“同一品种”。剩下的,不管是作为“同一棵树”的莓,还是作为“同一品种”的蓝莓,都放在一起计算:用总数除以\(k\)下取整。我们设\(dp[i][j]\),存一个\(\texttt{bool}\)值,表示考虑了前\(i\)棵树,是否能从每棵树上取若干个“同一品种”的红莓,使得取走的红莓数量\(\bmod k=j\),且剩下的红、蓝莓可以完美配对。这里完美配对的意思是,如果剩下的莓子总数为\(s\),则能装满\(\lfloor\frac{s}{k}\rfloor\)个篮子。转移时,枚举从当前树上取走\(x\)个红莓作为“同一品种”(\(0\leq x\leq \min(k-1,a_i)\))。设取走的数量为\(x\),则可以从\(dp[i-1][j]\),转移到\(dp[i][(j+x)\bmod k]\)。这样转移的前提是:要么\(x=a_i\)(即这棵树上不剩下任何红莓),要么\((a_i-x)\bmod k+b_i\geq k\)(即,剩下的红莓,能和剩下的蓝莓凑一筐“同一棵树”,再把多出来的蓝莓丢进蓝莓堆里)。
这个DP的潜在前提是,一定存在一种最优方案,使得每棵树上作为“同一棵树”的筐不超过\(1\)。这与我们第一个\(O(nk^4)\)暴力DP用到的结论是相对应的:是问题的两个极端,而贪心算法往往就要去考虑这种极端。
最后,枚举一个\(j\),若\(dp[n][j]=\texttt{true}\),则可以用\(\lfloor\frac{\sum_{i=1}^{n}(a_i+b_i)-j}{k}\rfloor\)更新答案。
时间复杂度\(O(nk^2)\)。
参考代码:
//problem:CF1348E
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MAXN=500;
int n,K,a[MAXN+5],b[MAXN+5];
bool dp[MAXN+5][MAXN+5];
int main() {
cin>>n>>K;
dp[0][0]=true;
ll sum=0;
for(int i=1;i<=n;++i){
cin>>a[i]>>b[i];
sum+=a[i]+b[i];
for(int j=0;j<K;++j)if(dp[i-1][j]){
for(int k=0;k<=a[i]&&k<K;++k){
if(k!=a[i]%K&&(a[i]-k)%K+b[i]<K)continue;
dp[i][(j+k)%K]=true;
}
}
}
for(int i=0;i<K;++i)if(dp[n][i]){
cout<<(sum-i)/K<<endl;return 0;
}
return 114514;
}
CF1348F Phoenix and Memory
如果只找出一组可行解,这是经典的贪心问题。我们把每个位置可选的值域区间(也就是\(n\)条线段)拿出来,按左端点从小到大排序。在值域上做扫描线。假设当前扫描到\(i\)。维护一个:左端点小于等于\(i\),且还未被使用的线段的集合。然后从集合里选出右端点最小的。在这条线段所表示的位置上填上值\(i\)。把该线段从集合里删除。然后继续考虑下一个值\(i+1\)。
这样就能求出一组解。
考虑再构造一组解,我们可以在已知解中交换两个位置上的值。
不妨枚举其中一个位置。设它上面填的值为\(x\),它可以填的值的范围为\([l,r]\)。显然,如果要找一个值和当前值交换,则这个值只可能在\([l,x-1]\)或\([x+1,r]\)中。且被交换的这个值,它所在位置的限制区间,必须包含\(x\)。具体来说,对于\([l,x-1]\)的值,它所在位置的限制区间,右端点必须\(\geq x\);对于\([x+1,r]\)的值,它所在位置的限制区间,左端点必须\(\leq x\)。于是问题转化为求序列区间最大/最小值。可以用st表或线段树维护。
时间复杂度\(O(n\log n)\)。
参考代码:
//problem:CF1348F
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MAXN=2e5;
int n,limL[MAXN+5],limR[MAXN+5],pos[MAXN+5],ans1[MAXN+5],ans2[MAXN+5];
struct Limits{
int l,r,pos;
bool operator<(const Limits& rhs)const{
return l<rhs.l;
}
}a[MAXN+5];
struct Rmq{
bool flag_max1min0;
int arr[MAXN+5],st[MAXN+5][21],_log2[MAXN+5];
void build(int n){
for(int i=1;i<=n;++i)st[i][0]=i;
for(int j=1;j<=20;++j){
for(int i=1;i+(1<<(j-1))<=n;++i){
if(flag_max1min0){
st[i][j]=(arr[st[i][j-1]]>=arr[st[i+(1<<(j-1))][j-1]]?st[i][j-1]:st[i+(1<<(j-1))][j-1]);
}
else{
st[i][j]=(arr[st[i][j-1]]<=arr[st[i+(1<<(j-1))][j-1]]?st[i][j-1]:st[i+(1<<(j-1))][j-1]);
}
}
}
_log2[0]=-1;
for(int i=1;i<=n;++i)_log2[i]=_log2[i>>1]+1;
}
pii query(int l,int r){
int k=_log2[r-l+1];
pii res;
if(flag_max1min0){
res.se=(arr[st[l][k]]>=arr[st[r-(1<<k)+1][k]]?st[l][k]:st[r-(1<<k)+1][k]);
}
else{
res.se=(arr[st[l][k]]<=arr[st[r-(1<<k)+1][k]]?st[l][k]:st[r-(1<<k)+1][k]);
}
res.fi=arr[res.se];
return res;
}
}rmxq,rmnq;
int main() {
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i].l>>a[i].r;
limL[i]=a[i].l;limR[i]=a[i].r;
a[i].pos=i;
}
sort(a+1,a+n+1);
multiset<pii>ms;
for(int i=1,j=1;i<=n;++i){
while(j<=n&&a[j].l==i){
ms.insert(mk(a[j].r,a[j].pos));
++j;
}
assert(!ms.empty());
pii p=*ms.begin();
ms.erase(ms.begin());
ans1[p.se]=i;
}
bool uniq=true;
rmxq.flag_max1min0=1;
rmnq.flag_max1min0=0;
for(int i=1;i<=n;++i){
assert(ans1[i]);
assert(!pos[ans1[i]]);
pos[ans1[i]]=i;
rmxq.arr[ans1[i]]=limR[i];
rmnq.arr[ans1[i]]=limL[i];
}
rmxq.build(n);
rmnq.build(n);
for(int i=1;i<=n;++i){
int l=limL[pos[i]];
int r=limR[pos[i]];
assert(i>=l&&i<=r);
if(l<i){
pii re=rmxq.query(l,i-1);
if(re.fi>=i){
uniq=false;
for(int j=1;j<=n;++j)ans2[j]=ans1[j];
swap(ans2[pos[i]],ans2[pos[re.se]]);
break;
}
}
if(r>i){
pii re=rmnq.query(i+1,r);
if(re.fi<=i){
uniq=false;
for(int j=1;j<=n;++j)ans2[j]=ans1[j];
swap(ans2[pos[i]],ans2[pos[re.se]]);
break;
}
}
}
if(uniq){
cout<<"YES"<<endl;
for(int i=1;i<=n;++i)cout<<ans1[i]<<" \n"[i==n];
}
else{
cout<<"NO"<<endl;
for(int i=1;i<=n;++i)cout<<ans1[i]<<" \n"[i==n];
for(int i=1;i<=n;++i)cout<<ans2[i]<<" \n"[i==n];
}
return 0;
}