Codeforces Round #661 (Div. 3)A-E2题解
//写于大号rating值2184/2184,小号rating值1662/1662
//打星场,打了21分钟后读错了E1的题意以为是要每条叶子到根的路径权值均不大于s
//想了几分钟没思路+肚子饿就撤了
//事实上E1是个很明显的贪心+优先队列。
//E2没想出来,看官方标程理解的思路
比赛链接:https://codeforces.com/contest/1399
过题用时:A题2min,B题4min,C题6min,D题9min,E1赛后补题用时约15-20min
A题
简单思维
给定一个数列,我们每次操作可以选择这个数列中差值不超过1的两个数,消去其中的一个数。询问是否能再若干次操作后让这个数列的长度变为1。
一个简单的结论,如果两个数字的差值大于1的,并且数列中不存在另外一个数字的值在这两个数字之间,那么这两个数字必然在最后会同时存在。
由此我们直接对数列sort一遍检查相邻数值有没有差值大于1即可。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
vector<ll>num(n);
for(auto &x:num) cin>>x;
sort(num.begin(),num.end());
bool flag=1;
for(ll i=1;i<n;i++)
if(num[i]-num[i-1]>1) flag=0;
if(flag) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
}
B题
贪心,整体思维
有n个礼物,每个礼物包括了a个糖果和b个橘子。现在的目标是使得这n个礼物的糖果数量都相同且橘子数量也都相同。
每次操作可以选择一个礼物,吃掉其中的1个糖果,或者吃掉其中的1个橘子,或者同时吃掉一个糖果加一个橘子。
现在需要你输出最少的操作次数。
我们注意到,对于同一个礼物来说,吃糖果和吃橘子实际上是互相之间不冲突的。我们把糖果和橘子分开来看(这个思想也运用在了E2上),分别采取贪心的策略。
初始的糖果数量当中的最小值就是我们最后构造出的糖果数量,因为如果要让最终的糖果数量都相等,那么大于这个初始最小值的糖果数量必须被减到等于这个初始最小值。而这个初始最小值又是最终可能的结果中最大的那一个,也就是最优的那个。
橘子同上。
由此我们先分别找到糖果和橘子的初始值中最小的那个,计算出对于每个礼物来说,需要吃掉numa个糖果,吃掉numb个橘子。由于吃糖果和橘子的操作不冲突,我们实际需要的操作次数是max(numa,numb),累加到最终答案ans上。
#include
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
vector<ll>a(n),b(n);
ll numa,numb;
ll mina=llINF,minb=llINF;
for(auto &x:a)
{
cin>>x;
mina=min(mina,x);
}
for(auto &x:b)
{
cin>>x;
minb=min(minb,x);
}
ll ans=0;
for(ll i=0;i<n;i++)
{
numa=a[i]-mina;
numb=b[i]-minb;
ans+=max(numa,numb);
}
cout<<ans<<endl;
}
}
C题
暴力
给定n个人的重量,现在要分配这些人参加划船比赛,每艘船上能坐且必须坐两个人,要求每艘船上的两个人的体重和要全部相等。问最多可以分配出几艘船。
我们注意到n的范围非常小,只有50,并且重量的大小也不超过n的范围。
因此我们记录一下重量为x的人有几个,记录为num[x],再直接暴力枚举每艘船上两个人的体重和sum(取2到100),暴力枚举第一个人的重量i(为避免重复计算这里规定第一个人的重量不大于第二个人),那么第二个人的重量就是sum-i,对应能凑出min(num[i],num[sum-i])艘船。
特别注意i==sum-i的情况,也就是两个人重量相同时,我们能凑出的船的数量应该是num[i]/2。
#include
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
ll num[110];
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
memset(num,0,sizeof(num));
for(ll i=0;i<n;i++)
{
ll x;
cin>>x;
num[x]++;
}
ll ans=0;
for(ll sum=2;sum<=100;sum++)
{
ll temp=0;
for(ll i=1;i*2<=sum;i++)
{
if(i*2!=sum) temp+=min(num[i],num[sum-i]);
else temp+=num[i]/2;
}
ans=max(ans,temp);
}
cout<<ans<<endl;
}
}
D题
实行,构造
给定一个只包含0和1的字符串,现在要求你把它分成若干个子串,每个子串中不能出现连续的0或者连续的1,要求输出满足以上条件的最少子串数量,并输出原字符串中每个字符对应位于哪一个下标的子串中。
这里使用两个栈odd和even,odd存储当前末尾数字为1的子串的下标,even存储当前末尾数字为0的子串的下标,now为当前总共有多少个子串。
cas[i]存储原字符串中第i个字符对应在第cas[i]个子串中
直接for一遍原字符串
扫到1就去检测even栈中是否为空,不为空的话就把这个值作为当前位置i的对应子串位置cas[i],并把这个值从even栈中取出放入odd栈中。如果even栈为空,则需要新增加一个子串,对now值+1后进行上述操作。
扫到0的操作同上。
#include
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=2e5+7;
ll cas[maxn];
ll n;
string s;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
cin>>n;
cin>>s;
ll now=0;
stack<ll>odd,even;
for(ll i=0;i<n;i++)
{
if(s[i]=='1')
{
if(even.size())
{
ll temp=even.top();
even.pop();
odd.push(temp);
cas[i]=temp;
}
else
{
cas[i]=++now;
odd.push(now);
}
}
else
{
if(odd.size())
{
ll temp=odd.top();
odd.pop();
even.push(temp);
cas[i]=temp;
}
else
{
cas[i]=++now;
even.push(now);
}
}
}
cout<<now<<endl;
for(ll i=0;i<n;i++)
{
if(i) cout<<' ';
cout<<cas[i];
}
cout<<endl;
}
}
E1题
贪心+优先队列
给定一棵树和一个限定值s,这棵树的每条边都有一个长度,现在需要累加每个叶子结点到根节点的路径长度和,定为sum,目标是使得sum的值不超过s。
我们每次操作可以使得一条边的长度变为原来的1/2,向下取整。需要求最少操作次数。
我们很容易得到这样一个结论,对于某一条确定的边[u,v]来说,我们规定从u-v是从根到叶子的方向,那么这条边对于在以v为根节点的子树中的所有叶子结点的路径来说,都会出现一次。
因此我们通过dfs遍历整棵树,在递归的过程中传递子树叶子结点的数量leaf,就是对应当前边的一个权值。
设当前边的长度为val,对应的叶子结点数量为leaf,那么我们对当前这条边进行一次操作后对sum的减少值就是(val-val/2) × \times ×leaf。我们采取贪心的策略,每次都选取当前整个影响值最大的边去操作就可以了。
具体实现通过优先队列,在dfs预处理后把所有边的长度值和对应叶子结点数量一同压入优先队列中。循环操作直到sum<=s为止。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=1e5+7;
ll n,s;
struct Edge
{
ll to,next,dis;
}edge[maxn*2];
ll head[maxn],tot;
void init()
{
for(ll i=1;i<=n;i++) head[i]=-1;
tot=0;
}
void add(ll u,ll v,ll w)
{
edge[tot].to=v;
edge[tot].next=head[u];
edge[tot].dis=w;
head[u]=tot++;
}
//前向星存图
struct Node
{
ll val,leaf;//val为当前边的权值,leaf为这条边对应子树的叶子结点数量
Node(ll val,ll leaf):val(val),leaf(leaf){}
friend bool operator <(Node a,Node b)//根据对该边进行一次divi操作后产生的效果排序,减少值更多的排在前面
{
return (a.val-a.val/2)*a.leaf<(b.val-b.val/2)*b.leaf;
}
};
priority_queue<Node>Q;//保存每条边对应的权值和子树叶子结点数量的优先队列,根据一次操作后的效果从大到小排序
ll sum,ans;
ll dfs(ll pre,ll now)//pre结点为上一个节点,避免反向递归,now为当前结点,返回值为以now结点为根节点的子树的叶子结点数量
{
ll leaf=0;//储存以当前结点为根的子树的叶子结点数量
for(ll i=head[now];i!=-1;i=edge[i].next)
{
ll to=edge[i].to;
if(to!=pre)
{
ll temp=dfs(now,to);//递归计算每个子树的叶子数量,并累加到leaf上
Q.push(Node(edge[i].dis,temp));//edge[i]这条边对应的子树的叶子结点数量就是temp
sum+=temp*edge[i].dis;//累加权值到sum上
leaf+=temp;
}
}
return leaf?leaf:1;//如果以当前结点为根的子树的叶子结点数量为0,代表自己就是一个叶子结点
}
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
while(Q.size()) Q.pop();
cin>>n>>s;
init();
for(ll i=1;i<n;i++)
{
ll u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
sum=ans=0;//sum为当前的所有路径的权值和,ans为我们需要进行的操作次数
dfs(0,1);
while(sum>s)
{
Node temp=Q.top();
Q.pop();
sum-=(temp.val-temp.val/2)*temp.leaf;
temp.val>>=1;
ans++;
Q.push(temp);
}
cout<<ans<<endl;
}
}
E2题
总体思路同E1,但是要把cost1和cost2的边看成两个部分,分别贪心预处理后求解。
在E1题意的基础上,每条边多了一个属性cost,其值为1或2,代表着当我们要对这条边进行一次操作时,需要消耗的硬币数量,现在我们希望我们消耗的硬币数量最少。
思考的时候进入了死胡同,一定要把cost为1和2的边放在一起考虑。但是实际上我们完全可以把这两类边分开来考虑。
我们最后进行了ans次的操作,那么其中的i次操作是对cost为1的边进行的,这i次操作对于cost为1的边来说,必然是和E1一样的最优贪心选择;对应的对于cost为2的边进行的j次操作,对于cost为2的边来说,也同样是和E1一样的最优贪心选择。
我们在E1的代码上略加修改,计cost为1的边对应的长度总和为sum1,计cost2的边对应的长度综合为sum2。
通过预处理出进行i次操作后sum1的值,j次操作后sum2的值。for一遍sum1的操作次数i,利用二分去寻找对应的sum2的操作次数j,取所有i+2j中最小的值即可。
#include
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=1e5+7;
ll n,s;
struct Edge
{
ll to,next,dis,cost;
}edge[maxn*2];
ll head[maxn],tot;
void init()
{
for(ll i=1;i<=n;i++) head[i]=-1;
tot=0;
}
void add(ll u,ll v,ll w,ll c)
{
edge[tot].to=v;
edge[tot].next=head[u];
edge[tot].dis=w;
edge[tot].cost=c;
head[u]=tot++;
}
//前向星存图
struct Node
{
ll val,leaf;
Node(ll val,ll leaf):val(val),leaf(leaf){}
friend bool operator <(Node a,Node b)
{
return (a.val-a.val/2)*a.leaf<(b.val-b.val/2)*b.leaf;
}
};
priority_queue<Node>Q1,Q2;//Q1为cost为1的边对应的长度和叶子结点数量,Q2为cost为2的边对应的长度和叶子结点数量
ll sum1,sum2,ans;//sum1为cost为1的边所产生的路径长度和,sum2为cost为2的边所产生的路径长度和
ll dfs(ll pre,ll now)
{
ll leaf=0;
for(ll i=head[now];i!=-1;i=edge[i].next)
{
ll to=edge[i].to;
if(to!=pre)
{
ll temp=dfs(now,to);
if(edge[i].cost==1)//此处与E1不同,根据cost不同分别压入两个不同的优先队列中
{
Q1.push(Node(edge[i].dis,temp));
sum1+=temp*edge[i].dis;
}
else
{
Q2.push(Node(edge[i].dis,temp));
sum2+=temp*edge[i].dis;
}
leaf+=temp;
}
}
return leaf?leaf:1;
}
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
cin>>n>>s;
init();
for(ll i=1;i<n;i++)
{
ll u,v,w,c;
cin>>u>>v>>w>>c;
add(u,v,w,c);
add(v,u,w,c);
}
sum1=sum2=0;
dfs(0,1);
deque<ll>cas1,cas2;
//cas1为对权值为1的点进行若干次操作后,sum1的值(逆序从小到大摆放方便二分)
//cas1的长度为len1,cas1[i]对应的就是进行len1-i-1次操作后,sum1的值
//cas2同上
cas1.push_front(sum1);
cas2.push_front(sum2);
ll temp=sum1;
while(temp)//对cost为1的边进行最优的贪心操作,预处理出cas1数组
{
Node now=Q1.top();
Q1.pop();
temp-=(now.val-now.val/2)*now.leaf;
now.val>>=1;
if(now.val) Q1.push(now);
cas1.push_front(temp);
}
temp=sum2;
while(temp)//对cost为2的边进行最优的贪心操作,预处理出cas2数组
{
Node now=Q2.top();
Q2.pop();
temp-=(now.val-now.val/2)*now.leaf;
now.val>>=1;
if(now.val) Q2.push(now);
cas2.push_front(temp);
}
ans=llINF;
ll len1=cas1.size(),len2=cas2.size();
for(ll i=0;i<len1;i++)//len1-i-1为对cost为1的边进行的操作次数,len2-j-1为对cost为2的边进行的操作次数
{
if(cas1[i]>s) break;
ll j=upper_bound(cas2.begin(),cas2.end(),s-cas1[i])-cas2.begin();//s-cas1[i]为sum2不能超过的值,通过二分查找得到下标
j--;
ans=min(ans,len1-i-1+2*(len2-j-1));
}
cout<<ans<<endl;
}
}