前言
沟槽的D卡了我一个多小时,快结束看了眼E发现E这么简单,结果就是比赛结束了才过了E,之后发现群友的D的思路是真的妙……还是太菜了T^T
一、A. Lever
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
vector<int>b(n+1);
for(int i=1;i<=n;i++)
{
cin>>b[i];
}
int ans=0;
while(true)
{
bool flag=true;
for(int i=1;i<=n;i++)
{
if(a[i]>b[i])
{
a[i]--;
flag=false;
break;
}
}
for(int i=1;i<=n;i++)
{
if(a[i]<b[i])
{
a[i]++;
break;
}
}
ans++;
if(flag)
{
break;
}
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
这个题可以看到数据量很小,那就直接模拟做就可以了。那就是每次迭代都先过一遍a数组,如果有一个比b数组大那就减小,然后直接break。之后再过一遍a数组,如果比b数组小就增加,然后break。最后看如果第一次操作没发生过那就直接退出输出答案即可。
二、B. Alternating Series
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
void solve()
{
int n;
cin>>n;
if(n%2==0)
{
for(int i=1;i<n;i++)
{
if(i%2==1)
{
cout<<-1<<" ";
}
else
{
cout<<3<<" ";
}
}
cout<<2<<endl;
}
else
{
for(int i=1;i<=n;i++)
{
if(i%2==1)
{
cout<<-1<<" ";
}
else
{
cout<<3<<" ";
}
}
cout<<endl;
}
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
这就是一个简单的构造题。
既然要求绝对值的字典序最小,那么贪心一下肯定是由-1开始,正负正负这样交替。又因为必须满足子数组的累加和是正数,所以对于中间的每个正数,累加和最小的情况肯定是选左右的负数组成的长度为三的子数组。又因为为了保证字典序最小,所以负数肯定都要选-1,那么只要中间的正数是3就一定能保证累加和是正数。而如果最后以正数结尾,那么因为右侧没有-1了,所以填2就能保证累加和是正数。
三、C. Make it Equal
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
void solve()
{
ll n,k;
cin>>n>>k;
vector<ll>s(n+1);
for(int i=1,x;i<=n;i++)
{
cin>>x;
s[i]=x%k;
}
multiset<ll>t;
for(int i=1,x;i<=n;i++)
{
cin>>x;
t.insert(x%k);
}
for(int i=1;i<=n;i++)
{
if(t.find(s[i])!=t.end())
{
t.erase(t.find(s[i]));
}
else if(t.find(k-s[i])!=t.end())
{
t.erase(t.find(k-s[i]));
}
else
{
cout<<"NO"<<endl;
return ;
}
}
cout<<"YES"<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
这题真的坑,上来看了半天没想明白,后来纯靠猜还真给猜过了。
观察发现,因为可以无限次加减k,所以其实这些数的原始数根本不重要,是需要考虑模k后的余数即可,就是把所有数都一直减k减到再减就小于0为止。这样操作后,对于可以通过反复加k相等的数,这两个数模k的余数肯定是一样的。之后,只需要过一遍s数组,每次考察这个余数和进行一次减k操作后的数在t中是否存在即可,如果两个都不存在就肯定没法凑成。
四、D. Arboris Contractio
还得加训树形结构……
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
const int MAXN=2e5+5;
void solve()
{
int n;
cin>>n;
vector<vector<int>>g(n+1);
for(int i=0,u,v;i<n-1;i++)
{
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
//叶节点的总个数
int sum=0;
//连接到同一个节点的叶节点数的最大值
int mx=0;
for(int i=1;i<=n;i++)
{
int tmp=0;
//自己就是叶节点
if(g[i].size()==1)
{
sum++;
tmp++;
}
for(int v:g[i])
{
//孩子是叶节点
if(g[v].size()==1)
{
tmp++;
}
}
mx=max(mx,tmp);
}
//选择有最多叶节点的节点为中心点
//操作数就是全部叶节点的个数sum减去最多叶节点的个数mx
cout<<sum-mx<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
群友的这个思路太妙了……
首先贪心一下,上来选择叶节点最多的节点肯定不会亏。因为整个过程就是每次把最长链上的节点都连到中心节点,那么每次剩下没连的就是叶节点。所以就是先考察每个节点,统计叶节点的总数和叶节点最多的节点的叶节点个数,并选择这个节点为中心节点。那么之后一次重连肯定只能解决一个叶节点,所以操作数就是叶节点的总数减去最多叶节点个数。
注意力惊人了属于是,赛时根本没往叶节点这方面考虑……
五、E. Adjacent XOR
赛时最后五分钟搓的,一塌糊涂但能过()
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
void solve()
{
int n;
cin>>n;
vector<ll>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
vector<ll>b(n+1);
for(int i=1;i<=n;i++)
{
cin>>b[i];
}
if(a[n]!=b[n])
{
cout<<"NO"<<endl;
return ;
}
vector<ll>dp(n+1);
dp[n]=a[n];
for(int i=n-1;i>=1;i--)
{
if(a[i]!=b[i])
{
if((a[i]^dp[i+1])==b[i])
{
dp[i]=a[i]^dp[i+1];
}
else if((a[i]^a[i+1])==b[i])
{
dp[i]=a[i]^a[i+1];
}
else
{
cout<<"NO"<<endl;
return ;
}
}
else
{
dp[i]=a[i];
}
}
cout<<"YES"<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
首先这个题可以发现修改操作只依赖右侧的值,那么对于最右侧的数,根本没法进行修改。所以就先判断最右侧的两个数,如果不同那就根本不可能凑成。因为每个操作只依赖右侧的值,那么可以考虑从后往前遍历。之后可以考虑dp一下,用dp数组存最终i位置的数。那么如果两数本来就相等,那根本就不用改,直接设置eor数组为原始数。如果不同的话,就需要修改了。修改的策略有两种,要么异或右侧原始的数,要么异或右侧修改后的数。那么就是如果跟右侧最终的数,即dp值异或后相等,那就设置当前位置的dp值为异或右侧dp值的结果。如果不行,那么如果跟右侧原始数异或能相等,就设置dp值为异或右侧原始值的结果。如果都不相等,那就根本不可能完成。
六、F. Unjust Binary Life
这b题是真的难……
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
//大于等于x的最左位置
int bs(ll x,int n,vector<array<ll,3>>&cnts)
{
int l=0;
int r=n-1;
int m;
int ans=n;
while(l<=r)
{
m=(l+r)/2;
if(cnts[m][0]>=x)
{
ans=m;
r=m-1;
}
else
{
l=m+1;
}
}
return ans;
}
void solve()
{
int n;
cin>>n;
string a,b;
cin>>a>>b;
//如果(i,j)->(i+1,j),那么有Ai^Bj=0,A(i+1)^Bj=0
//所以可以合并得到Ai^A(i+1)=0,即Ai==A(i+1)
//而如果(i,j)->(i,j+1),那么有Ai^Bj=0,Ai^B(j+1)=0
//所以可以得到Bj^B(j+1)=0,即Bj==B(j+1)
//又因为Ai^Bj=0,即Ai==Bj
//所以如果能从(1,1)->(i,j),必须有A1~Ai和B1~Bj的所有数全相等
//所以最小操作次数就是min(ones,zeros)
//所以可以考虑先预处理a串中从头到i的0的个数zeros和1的个数ones
//再根据zeros-ones从小到大排序,生成预处理数组help
//之后遍历b串,每次统计b串的zeros和ones
//去help里二分找大于等于b串ones-zeros的最左位置l
//此时对答案的贡献就是a串0~l上每个位置0的个数的累加和加上b串目前0的个数乘l
//再加上a串后续每个位置1的个数的累加和加上b串目前1的个数乘后续的长度
//举个例子,假如help={-1,-1,0,0}
//当来到b串的2位置,ones=2,zeros=1,ones-zeros=1
//说明b串到这个位置1的个数比0的个数多1
//所以说明a串中有四个位置补不上0比1少的个数,那么操作数就是0的个数
//那么对答案的贡献就是a串中0的个数加上b串目前0的个数乘以4
//再加上右侧操作数为1的个数的答案,即a串中后续1的个数加上b串目前1的个数乘以4-4=0
vector<array<ll,3>>cnts(n);
ll ones=0;
ll zeros=0;
for(int i=0;i<n;i++)
{
if(a[i]=='0')
{
zeros++;
}
else
{
ones++;
}
cnts[i]={zeros-ones,zeros,ones};
}
sort(cnts.begin(),cnts.end(),[&](const array<ll,3>&x,const array<ll,3>&y)
{
return x[0]<y[0];
});
vector<ll>pre0(n);
vector<ll>pre1(n);
pre0[0]=cnts[0][1];
pre1[0]=cnts[0][2];
for(int i=1;i<n;i++)
{
pre0[i]=pre0[i-1]+cnts[i][1];
pre1[i]=pre1[i-1]+cnts[i][2];
}
zeros=0;
ones=0;
ll ans=0;
for(int i=0;i<n;i++)
{
if(b[i]=='0')
{
zeros++;
}
else
{
ones++;
}
int pos=bs(ones-zeros,n,cnts);
if(pos==0)
{
ans+=pre1[n-1]+ones*n;
}
else
{
ans+=pre0[pos-1]+zeros*pos+pre1[n-1]-pre1[pos-1]+ones*(n-pos);
}
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
这个题上来的这个思路就想不到。根据定义,假如能从(i,j)走到(i,j+1),那么因为Ai^Bj等于0,又因为Ai^B(j+1)等于0,所以把这两个式子再异或起来可以得到Bj^B(j+1)等于0。同理可以得到Ai^A(i+1)等于0,所以如果能从(1,1)走到(i,j),那么必须有从A1到Ai,从B1到Bj,所有的数都完全一样。那么要保证能走到的最小操作数就是这些数字里,0的个数和1的个数的最小值。所以,所有操作的代价就是对于所有的二元对(i,j)的最小操作数。
之后就需要考虑如何将复杂度降下来了。考虑先遍历一遍a串,每次统计从头到当前i位置,0的个数zeros和1的个数ones,以及zeros减ones的个数。之后,根据zeros减ones从小到大排序,然后分别求整个数组里zeros和ones的前缀和。之后,遍历b串,同样统计从头到当前位置的zeros和ones。
之后是重点,举个例子,假如b串从头到当前位置的zeros为2,ones为4,那么ones减zeros就是2,表明1的个数比0的个数多2个。之后,去之前a串统计出的数组里,二分查找大于等于2的最左位置pos。那么对于这个位置pos,左侧部分都是zeros减ones小于等于2的,即0的个数和1的个数弥补不了b串当前的差距,所以此时合起来0的个数和1的个数的最小值就是0的个数。同理,这个位置pos的右侧位置都是zeros减ones大于2的,即合起来后1最小值是1的个数。所以,0的个数和1的个数可以直接从前缀和数组里查,再加上b串当前的zeros和ones乘以对应个数即可。
真的离谱这个思路……
七、G. Wafu!
这个G感觉也不过如此嘛。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;
const int MOD=1e9+7;
//找小于等于的最右位置
int bs(ll x,int n,vector<ll>&cnts)
{
int l=1;
int r=n;
int m;
int ans=0;
while(l<=r)
{
m=(l+r)/2;
if(cnts[m]<=x)
{
ans=m;
l=m+1;
}
else
{
r=m-1;
}
}
return ans;
}
void solve()
{
ll n,k;
cin>>n>>k;
vector<ll>s(n+1);
for(int i=1;i<=n;i++)
{
cin>>s[i];
}
sort(s.begin()+1,s.end());
//cnts[i]:1~i乘完的次数
vector<ll>cnts(32);
//mult[i]:1~i乘完
vector<ll>mult(32);
cnts[0]=0;
mult[0]=1;
for(int i=1;i<32;i++)
{
cnts[i]=cnts[i-1]*2+1;
mult[i]=((mult[i-1]*mult[i-1])%MOD*i)%MOD;
}
ll ans=1;
int i=1;
while(k>0)
{
if(i<=n&&s[i]-1<32&&k>=1+cnts[s[i]-1])//能乘数组里的并把新增的乘完
{
ans=(ans*s[i])%MOD;
ans=(ans*mult[s[i]-1])%MOD;
k-=1+cnts[s[i]-1];
i++;
}
else
{
//还能乘数组里的
if(i<=n)
{
ans=(ans*s[i])%MOD;
k--;
i++;
}
//只能乘新增的
int pos=31;
while(k>0)
{
//二分找最多乘到的位置
pos=bs(k,pos,cnts);
ans=(ans*mult[pos])%MOD;
k-=cnts[pos];
//还能再乘一次
if(k>0)
{
ans=(ans*(pos+1))%MOD;
k--;
}
}
}
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
首先,因为每次从集合里删除一个数后,都要把从1到这个数中间的所有数都加入集合。又因为每次都选集合中最小的数,所以每次肯定会优先处理新加入的数。那么就可以考虑用dp的思想,先预处理一个cnts数组,cnts[i]表示集合的前i个数正好为1~i,把这i个数删完需要的次数。再预处理一个mult数组,其中mult[i]还是表示集合的前i个数正好为1~i,把这i个数删完答案一共要乘的数。再观察可以发现,在删除的过程中,在删第i个数i之前,肯定要把1~i-1删完,之后才能删i。接着因为又加入了1~i-1,所以还得再删一遍1~i-1,那么就可以得到如上面写的转移公式。通过观察cnts[i]的数可以发现,cnts[i]其实就等于2^i-1。又因为k小于等于10^9,没超过int的范围,所以cnts和mult都只需要开32长度即可。又因为每次在删完集合中的数x后,都要处理1~x-1的部分,那么只有当这个x-1小于32时,才有可能把新增的全删完。
之后,在对数组从小到大排序后,只要k大于0,那么如果数组里还有数,即原本的集合没被删完,且集合中的数-1小于32,即能把新增的全删完,且k的次数比删完一轮的次数还大,那就先删集合内的数,再把1~x-1全删完。否则,说明k不够全删完的了,那如果此时集合里还有数,那就先删集合里的。之后,因为新增的数删不完,只要k还大于0,那么每次能完整删完的1~i就可以在cnts里二分查找小于等于k的最右位置,然后把1~i这一部分全删了。如果还能再删一次,那么就再把数字i+1给删了,去后续看能否接着删了。
总结
一步一步来,天道酬勤,加油!!
1548

被折叠的 条评论
为什么被折叠?



