A
每次可以选一个子区间,复制其中所有元素,那其实我们可以用这个操作把初始元素变成任意多个,但是我们不能凭空变出来一个元素。那么给出操作后的数组,其中每一段连续相同元素,一定是初始数组中至少一个该种元素操作得来的。因此把每一段连续相同元素缩成一个该种元素,就是最小的初始数组
void solve(){
cin>>n;
vi a(n+1);
rep(i,1,n)cin>>a[i];
int ans=1;
rep(i,2,n){
if(a[i]!=a[i-1]){
ans++;
}
}
cout<<ans<<'\n';
}
B
经典子序列+状态机dp
给一个序列,定义只有两个数都不减才是合法的,可以交换一个元素的xy,也可以删除一个元素,问使序列合法的最小代价。
注意数据,支持我们,那可以写一个朴素转移的子序列dp,
表示得到以
为结尾的合法子序列的最小代价,枚举上一个位置,只要满足和上一个位置的元素满足前面说的合法条件,就可以转移,中间的元素都删掉。
然后还有一个交换xy的操作,那么我们可以加一个维度表示这个状态是否反转了最后一个元素
然而这个状态设计还是有一点问题,因为是默认最后一个元素必须选的,那如果返回实际上选上了第n个元素,但是最优方案可能不选第n个元素。可能是把一段后缀删掉了。
这里我选择的解决方式是类似哨兵的思想,加了一个第n+1个元素,然后这个元素的xy都很大,可以从前面任何一个位置转移过来,那么这时就可以包含删除一个后缀的情况了
void solve(){
int x,y;
cin>>n>>x>>y;
vi a(n+2),b(n+2);
rep(i,1,n)cin>>a[i]>>b[i];
a[n+1]=b[n+1]=2e9;
vvi dp(n+2,vi(2,1e15));
dp[1][0]=0;
dp[1][1]=y;
rep(i,2,n+1){
rep(j,1,i-1){
rep(p,0,1){
int a1=a[j],b1=b[j];
if(p)swap(a1,b1);
rep(q,0,1){
int a2=a[i],b2=b[i];
if(q)swap(a2,b2);
if(a1<=a2&&b1<=b2){
dp[i][p]=min(dp[i][p],dp[j][q]+p*y+x*(i-j-1));
}
}
}
}
dp[i][0]=min(dp[i][0],(i-1)*x);
dp[i][1]=min(dp[i][1],(i-1)*x+y);
//cout<<dp[i][0]<<' '<<dp[i][1]<<'\n';
}
cout<<min(dp[n+1][0],dp[n+1][1])<<'\n';
}
C
数列题不会都可以考虑打表。
这题打表发现出现的数很多,然后发现只有少数几个数不出现,且4,8,16都没出现,那么转而对不出现的元素打表,发现从4开始的2的幂都不出现。
那么问第k小元素,可以二分,检查一个小于x的数的个数-小于x的2的幂的个数,是否小于k。直接求也可以,但是要讨论,感觉有点麻烦
void solve(){
cin>>n;
int l=1,r=2e18;
auto check=[&](int x)->bool{
int cnt=x-1;
int p=4;
while(p<2*x){
cnt--;
p*=2;
}
return cnt<n;
};
while(l<=r){
int m=l+r>>1;
if(check(m))l=m+1;
else r=m-1;
}
cout<<r*2<<'\n';
}
D
贡献法+期望
代码很短,思路很长。
首先为什么要贡献法?对于这种有很多个方案,然后求每个方案的某个函数值之和的问题,枚举方案然后加起来会超时,但是每个方案包含的基本元素并不多,那么可以转而枚举每个基本元素,然后计算每个基本元素的贡献,也就是它会被多少个方案所包含,这样会快的多,也好想。
然后对于这个题,我们要用贡献法,首先要找到基本元素,一个比较显然的元素是连通块,但是连通块并不容易枚举,那么有什么和连通块等价的东西吗,我们枚举他们也行?
手玩/推理可以发现每个连通块里有且只有一个边,同时是
的最小边。可以反证:如果一个连通块有两个这样的边
,不妨设
,那么根据选边的规则,b会选呃e1,u会选e2,那么bu就不不会联通了,这就不是一个连通块了,矛盾。
那么连通块和这种边是等价的,那么求连通块的期望,就是求这样的边的个数的期望,这又等价于,每条边成为这样的边的方案数的期望的和。
那么我们可以枚举边,然后算每条边的期望然后求和。每条边都是他所连接的两个点的所有边中的一条,在边权赋值完全随机的情况下,他成为最小边的概率就是
,他成为最小边的方案数的期望也是
。
然后每条边的期望都是这样算的,一共条边,因此答案就是
void solve(){
mint x;
cin>>x;
cout<<x*(x-1)/(4*x-6)<<'\n';
}
E
单调栈+暴力
保证数据随机是个很好的性质,在这个条件下很多东西的期望复杂度都并不大,因此可以用偏暴力的方法来做,似乎去年鸡哥也出了类似的东西。
对于本题,随机生成的一个序列,相同的段数的期望是
的,因此我们可以单调栈记录每个
变化的分界点,然后对枚举分界点形成的区间,统计答案。这里的具体实现是用set存所有分界点,然后枚举就行了,可能有重复点,但是区间长度为0,对答案贡献也是0,不影响。
我们统计了每种的值的出现次数,最后答案问的是
大于某个值的区间个数,因此我们还需要对这个值求个后缀和,然后每次查询就直接二分就可以了。这里求后缀和需要倒序遍历,可以枚举迭代器,这里学了一个新写法,先用
,然后用
,可以直接倒序遍历
void solve(){
cin>>n>>m>>k;
vi a(n+1);
rep(i,1,n)cin>>a[i];
stack<int>s1,s2;
multiset<int>s;
map<int,int>ans;
rep(i,1,n){
while(s1.size()&&a[i]>=a[s1.top()])s.erase(s.find(s1.top())),s1.pop();
while(s2.size()&&a[i]<=a[s2.top()])s.erase(s.find(s2.top())),s2.pop();
if(!s1.size())s.insert(0);
if(!s2.size())s.insert(0);
s1.push(i);
s2.push(i);
s.insert(i);
s.insert(i);
int mx=0,mn=1e9;
for(auto p=next(s.rbegin());p!=s.rend();++p){
int pre=*prev(p);
int cur=*p;
mx=max(mx,a[pre]);
mn=min(mn,a[pre]);
ans[mn*mx]+=pre-cur;
}
}
debug(ans);
ans[1e18]=0;
int sum=0;
for(auto &[k,v]:ans|views::reverse){
sum+=v;
v=sum;
}
rep(i,1,k){
int x;
cin>>x;
cout<<ans.lower_bound(x)->second<<'\n';
}
}
F
贪心,最害怕的一集。还是好几个贪心拼在一起的
首先注意到我们能取的答案就是在里取的,因此我们可以把
排序,然后枚举最后取到的最大的a+b,然后检验这个值能不能通过优化面试顺序取到。那么对于前面更小的a+b,我们要安排他们的顺序,让能获得的offer尽量大,如果最后能大于当前
,就能取到当前
。
如果不大于,那么说明当前a比前面的所有
都更大,那么我们可以把这个
放到最开头,这样就能取到前面最大的
。
void solve(){
cin>>n;
vi a(n+1),b(n+1);
rep(i,1,n)cin>>a[i]>>b[i];
vvi c;
rep(i,1,n){
c.push_back({a[i]+b[i],a[i]});
}
sort(c.begin(),c.end());
vi ans(n+1),mx(n+1);
rep(i,1,n){
ans[i]=ans[i-1];
if(ans[i-1]>=c[i-1][1]){//如果前面最大的大于当前a,取到a+b
ans[i]=max(ans[i],c[i-1][0]);
}
else{//如果不大于,把这个a安排到第一个,取到前缀最大a+b
ans[i]=max(ans[i],mx[i-1]);
}
ans[i]=max(ans[i],c[i-1][1]);//a肯定能取到
mx[i]=max(mx[i-1],c[i-1][0]);//最大的a+b
}
cout<<ans[n]<<'\n';
}
H
构造,根本想不到,就当开拓眼界了
首先如果所有区间大小是偶数,那直接就可以。如果是奇数,可能出现区间中点的数排序之后位置没变,不能用这个方案了。
那把这个问题一般化一点,想染所有长度为奇数的区间排序后都变了,这个每个奇数区间在排序前都必须是个错排,也就是没有一个元素是在排序后的位置上的,这样排序一下的新位置都会和排序前不同。
然后所有奇数大小的区间,都是由最小的奇数区间,也就是长为3的区间构成的(长为1显然没意义),那么我们只要保证每个长度为3的区间都是错排,就能保证所有长为奇数的区间都是错排了。
注意到长为3的错排有两种,而且他们的前后缀还是连着的。那其实就可以按
这样的模式构造,注意这里的数字是他们在长度为3的子数组中的相对大小。
这个思路的实现有一种简单的方法,就是把每两个交换一下,例如
void solve(){
cin>>n>>m;
vi a(n+1);
rep(i,1,n)a[i]=n-i+1;
rep(i,1,m){
int l,r,c;
cin>>l>>r>>c;
k=(r-l+1)%2;
}
if(k%2){
for(int i=1;i<n;i+=2){
swap(a[i],a[i+1]);
}
}
rep(i,1,n)cout<<a[i]<<' ';
cout<<'\n';
}
I
离线+线段树或者在线+主席树
我们要求一个区间内的一个元素,排序后是不是还在这个位置,实际上就是求这个区间内小于这个元素的个数,这个东西很容易联想到主席树,但是主席树板子是求第k打的元素是几,这个是求小于k的元素的个数,略有区别,那么可以去修改一下查询方式,或者仍然使用查第k大的板子,外面套个二分,第二种方法是的,会爆。
离线就比较好做。查一个区间内小于K的元素个数,我们可以把询问按K大小离线,然后每次查的时候,ds里只有小于当前K的元素,那就直接查区间内元素个数就行了。
也可以注意到内小于K的元素个数,是等于
的结果的,那么我们可以把询问拆开,然后从左到右把元素加入ds,到了一个位置
,处理所有有一个端点在
的所有询问,如果是
就减,是
就加。这只需要一个求小于K的元素个数的ds,可以是权值线段树,也可以是treap。
void solve(){
cin>>n>>m;
vi a(n+1);
rep(i,1,n)cin>>a[i];
vvi b[n+1];
vi ans(m+1);
rep(i,1,m){
int l,r,c;
cin>>l>>r>>c;
ans[i]+=l-1;
b[l-1].push_back({i,a[c],-1});
b[r].push_back({i,a[c],1});
}
RedBlackTree tr;
rep(i,1,n){
tr.insert(a[i]);
for(auto &q:b[i]){
int id=q[0],v=q[1],sym=q[2];
int cnt=tr.countLessEqual(v);
ans[id]+=sym*cnt;
}
}
rep(i,1,m)cout<<ans[i]<<'\n';
}
J
枚举+贪心
首先注意到这个过程一定是先只磨刀,然后又磨又打。如果磨一会,打一会,肯定是不如先把刀磨到较高攻击力再打要划算的。
然后注意到磨刀石不多,也就是只磨不打的可能时长不多,那么可以枚举只磨不打的阶段的时长,然后剩下的时间先有磨又打,磨刀石没了就只打。对每种情况取max即可。
然后这题似乎可以三分,因为这个总伤害似乎是凸函数,这样就可以做了
void solve(){
int x,y;
cin>>n>>x>>y;
int ans=0;
rep(i,0,min(y,n)){
int mx=x+i;
int t1=min(n,y)-i,t2=min(n-t1-i,mx);
int cur=t1*(mx+1);
cur+=(mx+mx-t2+1)*t2/2;
ans=max(ans,cur);
}
cout<<ans<<'\n';
}
K
注意到翻一页,两页之和+4,那么就看目标和和当前的两页的和模4是否同余
void solve(){
cin>>n>>m;
if(m%4==(2*n+1)%4){
cout<<"YES\n";
}
else{
cout<<"NO\n";
}
}
L
每次都删两个不同元素,可以在任意位置,问能不能剩下一个给定字符串。
首先这个字符串必须是初始串的子串。然后非字符串中的必须都删了,而且每次只能删两个不同的,那这其实就转化成经典贪心,不同元素两两配对,问能不能把所有元素都配对。
这需要看出现次数最多的元素,出现次数超不超过总数的一半,不超过就可以,超过则不行。
void solve(){
cin>>n;
string s;
cin>>s;
s=' '+s;
string t="CHICKEN";
int pos=0;
rep(i,1,n){
if(pos<7&&s[i]==t[pos]){
pos++;
}
}
if(pos!=7){
cout<<"NO\n";
}
else if((n-7)%2==1){
cout<<"NO\n";
}
else{
map<char,int>mp;
rep(i,1,n){
mp[s[i]]++;
}
rep(i,0,6){
mp[t[i]]--;
}
int mx=0;
for(auto &[k,v]:mp){
mx=max(mx,v);
}
if(mx>(n-7)/2){
cout<<"NO\n";
}
else{
cout<<"YES\n";
}
}
}