AtCoder Beginner Contest 407(ABCDEF)
前言
最近能感觉到是有进步的,尤其是第三个题,加油加油!!
一、A - Approximation
#include <bits/stdc++.h>
using namespace std;typedef long long ll;
typedef pair<int,int> pii;void solve()
{int a,b;cin>>a>>b;double A=a,B=b;int up=(a+b-1)/b;int down=a/b;if(up-A/B<0.5){cout<<up;}else{cout<<down;}
}int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;//cin>>t;while(t--){solve(); }return 0;
}
签到题,没啥好说的。
二、B - P(X or Y)
#include <bits/stdc++.h>
using namespace std;typedef long long ll;
typedef pair<int,int> pii;void solve()
{int x,y;cin>>x>>y;double sum=0;for(int i=1;i<=6;i++){for(int j=1;j<=6;j++){if(i+j>=x||abs(i-j)>=y){sum++;}}}double p=sum/36;cout<<fixed<<setprecision(10)<<p;
}int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;//cin>>t;while(t--){solve(); }return 0;
}
这个就是个概率题,就是枚举两个骰子可能出现的所有情况,只要至少有一个条件满足就统计,然后除以总情况数36即可。注意最后要控制输出精度。
三、C - Security 2
#include <bits/stdc++.h>
using namespace std;typedef long long ll;
typedef pair<int,int> pii;void solve()
{string s;cin>>s;int n=s.length();int cnt=0;for(int i=n-1;i>=0;i--){cnt+=(s[i]-'0'-cnt%10+10)%10;}cout<<cnt+n;
}int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;//cin>>t;while(t--){solve(); }return 0;
}
这个题其实也不是太难,重点是思路有点抽象。
观察一下就能发现,因为每次添加必然是在最后添加,而在添加前可以任意更改前面的数,所以考虑用“时间倒流”的思想,从后往前遍历数组。又因为在添加前可以任意更改前面的数,所以要保证前面的数和新添加的数的差值一样,举个例子,假设是“2025”这个数,因为最后“5”和“2”差3,所以在添加最后一位前要保证倒数第二位是“7”,这样添加一个“0”后"70"就能一起转到“25”。所以就是每步计算当前位需要几次才能从0转到当前数字,之后的每一步都需要在之前转过的次数上减去之前的影响。
怎么说呢,感觉这个思路是真的抽象……
四、D - Domino Covering XOR
#include <bits/stdc++.h>
using namespace std;typedef unsigned long long ll;
typedef pair<int,int> pii;const int MAXN=22;ll grid[MAXN][MAXN];
int uncover;//h*w<=20 -> 对棋盘进行状态压缩ll dfs(ll sum,int h,int w)
{ll ans=sum;for(int i=1;i<=h;i++){for(int j=1;j<=w;j++){//byd括号战神是吧if(i+1<=h&&((uncover&(1<<((i-1)*w+j)))==0)&&((uncover&(1<<(i*w+j)))==0)){uncover^=(1<<((i-1)*w+j));uncover^=(1<<(i*w+j));ans=max(ans,dfs(sum^grid[i][j]^grid[i+1][j],h,w));uncover^=(1<<((i-1)*w+j));uncover^=(1<<(i*w+j));}if(j+1<=w&&((uncover&(1<<((i-1)*w+j)))==0)&&((uncover&(1<<((i-1)*w+j+1)))==0)){uncover^=(1<<((i-1)*w+j));uncover^=(1<<((i-1)*w+j+1));ans=max(ans,dfs(sum^grid[i][j]^grid[i][j+1],h,w));uncover^=(1<<((i-1)*w+j));uncover^=(1<<((i-1)*w+j+1));}}}return ans;
}//dfs暴力未剪枝 -> 超时
void solve1()
{int h,w;cin>>h>>w;ll sum=0;uncover=0;for(int i=1;i<=h;i++){for(int j=1;j<=w;j++){cin>>grid[i][j];sum^=grid[i][j];}}cout<<dfs(sum,h,w);
}//bfs正解
void solve2()
{int h,w;cin>>h>>w;for(int i=0;i<h;i++)//下标从0开始方便状态压缩{for(int j=0;j<w;j++){cin>>grid[i][j];}}queue<int>q;q.push(0);//空棋盘vector<bool>vis(1<<MAXN);//剪枝!!!vis[0]=true;//多米诺牌的位信息int dh=0b11,dv=(1<<w)+1;while(!q.empty()){int cur=q.front();q.pop();//水平int next;for(int i=0;i<h;i++){for(int j=0;j<w-1;j++){if((cur&(dh<<(i*w+j)))==0){next=cur|(dh<<(i*w+j));if(!vis[next]){vis[next]=true;q.push(next);}}}}//竖直for(int i=0;i<h-1;i++){for(int j=0;j<w;j++){if((cur&(dv<<(i*w+j)))==0){next=cur|(dv<<(i*w+j));if(!vis[next]){vis[next]=true;q.push(next);}}}}}ll ans=0;for(int s=0;s<(1<<(h*w));s++){if(vis[s])//能放出来{ll sum=0;for(int i=0;i<h;i++){for(int j=0;j<w;j++){if((s&(1<<(i*w+j)))==0)//没被覆盖{sum^=grid[i][j];}}}ans=max(ans,sum);}}cout<<ans;
}int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;//cin>>t;while(t--){solve2(); }return 0;
}
这个题赛时写的是暴力dfs枚举,然后喜提TLE,后来听讲解的时候发现可以对棋盘进行状态压缩,然后改了以后发现还是TLE,最后才发现需要挂缓存剪枝才行……
这里正解是用的bfs,像刚刚说的,因为hw最多就是20,所以考虑对棋盘进行状态压缩,即先给每个格子按顺序编号,然后用位信息去存是否被覆盖。之后还需要用一张vis表剪枝,没来过的状态才会考虑。
之后,为了方便快速判断是否能覆盖多米诺骨牌,可以也用位信息表示两种骨牌。所以水平放置的就是“11”,而横向放置的就是1先左移w位再加1。那么之后对于放置的位置就是让骨牌移到对应位置,即左移i*w+j位。
所以bfs的过程就是准备个队列,然后每次从队列里拿状态。若当前棋盘状态与上骨牌等于0,说明可以放置,那么之后的状态就是把骨牌或进去,没来过的话就加入队列。最后枚举每个棋盘的状态,若vis为true,即能被放出来,那就遍历棋盘把没被覆盖的地方都异或起来统计最大值即可。
五、E - Most Valuable Parentheses
#include <bits/stdc++.h>
using namespace std;typedef long long ll;
typedef pair<int,int> pii;//动态规划超时
void solve1()
{int n;cin>>n;n*=2;vector<ll>nums(n);for(int i=0;i<n;i++){cin>>nums[i];}vector<ll>dp(n);for(int i=n-2;i>=0;i--){ ll leftDown=dp[0];dp[0]=nums[i]+dp[1];for(int j=1;j<n-i;j++){ll tmp=dp[j];dp[j]=max(nums[i]+dp[j+1],leftDown);leftDown=tmp;}}cout<<dp[0]<<endl;
}//贪心解
void solve2()
{int n;cin>>n;vector<ll>nums(n<<1);for(int i=1;i<=2*n;i++){cin>>nums[i];}//对于合法的括号序列2n长度 -> 对于每个前缀,里面的左括号数肯定大于等于右括号数//根据这条原则://对于2n个数//nums[1]必选 -> (//nums[2],nums[3]里至少选一个 -> ((),(((,()(//nums[1~5]里至少选三个//nums[1~7]里至少选五个//……//num[1~2n-1]里至少选n个 -> 堆priority_queue<int>heap;//大根堆ll ans=nums[1];for(int i=2;i<2*n;i++){heap.push(nums[i++]);heap.push(nums[i]);ans+=heap.top();heap.pop();}cout<<ans<<endl;
}int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int t=1;cin>>t;while(t--){solve2(); }return 0;
}
这个题赛时看到直接就去dp了,直接忘记分析时间复杂度了,然后就喜提TLE……因为dp复杂度至少O(n^2),而题目数据量肯定不允许。
正解用的是贪心,观察合法的括号序列的特点可以发现,对于每个前缀,里面的左括号数量必然大于等于右括号数量。所以根据这个原则可以推出,第一个位置必然是左括号,之后第二第三个位置必然有一个是左括号,即1~3里至少有两个左括号,所以之后1~5里至少三个,1~7里至少五个,之后以此类推。
所以考虑用一个大根堆维护每个位置,每次往里加入两个,然后让ans加上堆顶即可。
怎么说呢,这个贪心过程里有反悔的部分,即当新加入的两个都不是最大时,可以对之前的选择进行反悔,改为去选之前的元素。
六、F - Sums of Sliding Window Maximum
#include <bits/stdc++.h>
using namespace std;typedef long long ll;
typedef pair<int,int> pii;void solve()
{int n;cin>>n;vector<ll>nums(n+1);for(int i=1;i<=n;i++){cin>>nums[i];}//贡献法:假设M为某个区间内的最大值,则M会对整个区间的所有包括M的子数组都有贡献// e.g. (?,?,M,?,?,?,?)// L i R// k=1,1 k=2,2 k=3,3 k=4,3 k=5,3 k=6,2 k=7,1// l=i-L=3 r=R-i=5// k=1~l-1 -> k次 k=l~r -> l次 k=r+1~l+r-1 -> l+r-k次 // 所以,对于每个数nums[i],其有贡献的区间为左右两侧最近的比它大的数 -> 单调栈// 而对于求每个数的贡献,其贡献的次数类似等腰梯形 -> 等差数列+一维差分stack<int>idx;//单调栈 -> 小压大//求Lvector<int>L(n+1);for(int i=1;i<=n;i++){while(!idx.empty()&&nums[i]>=nums[idx.top()]){idx.pop();}if(!idx.empty()){L[i]=idx.top();}else{L[i]=0;}idx.push(i);}//清空栈while(!idx.empty()){idx.pop();}//求Rvector<int>R(n+1);for(int i=n;i>=1;i--){while(!idx.empty()&&nums[i]>nums[idx.top()])//定义相等时认为左侧较小{idx.pop();}if(!idx.empty()){R[i]=idx.top();}else{R[i]=n+1;}idx.push(i);}vector<ll>ans1(n+5);//一维差分 //多开点防溢出vector<ll>ans2(n+5);//等差数列差分for(int i=1;i<=n;i++){ll l=i-L[i];ll r=R[i]-i;if(l>r){swap(l,r);}//自己写的暴力解 -> nums需要开ll// //等差数列差分// ans2[1]+=1*nums[i];// ans2[2]+=0*nums[i];// ans2[l]-=l*nums[i];// ans2[l+1]+=(l-1)*nums[i];// //一维差分// ans1[l]+=l*nums[i];// ans1[r+1]-=l*nums[i];// //等差数列差分// ans2[r+1]+=(l-1)*nums[i];// ans2[r+2]+=(-l)*nums[i];// ans2[l+r]-=0*nums[i];// ans2[l+r+1]+=1*nums[i];//优化解ans2[1]+=nums[i];ans2[l+1]-=nums[i];ans2[r+1]-=nums[i];ans2[l+r+1]+=nums[i];}//自己写的暴力解// //求两遍前缀和// for(int i=0;i<2;i++)// {// for(int j=1;j<=n;j++)// {// ans2[j]+=ans2[j-1];// }// }// //求一遍前缀和// for(int i=1;i<=n;i++)// {// ans1[i]+=ans1[i-1];// }// //统计答案// for(int i=1;i<=n;i++)// {// cout<<ans1[i]+ans2[i]<<endl;// }//优化解vector<ll>ans(n+5);for(int i=1;i<=n;i++){ans1[i]=ans1[i-1]+ans2[i];ans[i]=ans[i-1]+ans1[i];cout<<ans[i]<<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和贡献次数的关系类似一个梯形,即贡献次数随k一开始呈等差数列上升,之后在某个k的区间内不变,最后呈等差数列下降。再分析可以发现,保持不变的区间左右端点,就是左侧包括当前位置的区间长度和右侧包括当前位置的区间长度。
之后就是考虑如何实现这个思路。观察发现,对于每个位置,区间的左右端点就是左右两侧大于当前位置的最近位置,那就是单调栈解决的问题了。又因为要在O(1)的时间内快速计算出贡献次数,那就可以考虑用一维差分和等差数列差分,然后构造前缀和解决。
所以之后就是先用单调栈从前往后和从后往前滑两遍求出每个位置的L和R。再遍历一遍差分,最后求两遍前缀和即可。
总结
还是要多培养求“贡献”的思想,加油加油!!
END