比赛链接
官方题解(视频)
这场偏思维,感觉好像没啥算法。E需要动态维护前 k k k 小,F是个离散化加dp,状态和递推方程比较 非常规,建议还是看一下涨涨姿势。
A 小A的文化节
思路:
签到
code:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=105;
int n,m,a[maxn];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
int ans=0;
for(int i=1,t;i<=m;i++){
cin>>t;
ans+=a[t];
}
cout<<ans;
return 0;
}
B 小A的游戏
思路:
只有两种加分方式,一种是一个人加 3 3 3 分,另一个人加 0 0 0 分,一种是两人各加 1 1 1 分.。
发现如果没有平局的话,两人分数一定是 3 3 3 的倍数,加入平局后,两人分数的差值仍然是 3 3 3 的倍数。反过来,如果分数的差值是 3 3 3 的倍数的话,就可以凑出来某人赢了几场,平了几场,输了几场。换句话说,就是两人分数之差为 3 3 3 的倍数=有解。
code:
#include <iostream>
#include <cstdio>
using namespace std;
int T,a,b;
int main(){
cin>>T;
while(T--){
cin>>a>>b;
puts((abs(a-b)%3==0)?"Yes":"No");
}
return 0;
}
C 小A的数字
思路:
看半天以为是二进制位,结果是十进制位。。。
当然是尝试贪心地将每个数位都置为 0 0 0,如果本来就是 0 0 0 就置为 1 1 1。因为要求是正整数,所以结果为 0 0 0 的时候还需要特判。
code:
#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
int T,n;
int main(){
cin>>T;
while(T--){
cin>>n;
int flag=n%10;
stack<int> s;
while(n){
s.push((n%10)?0:1);
n/=10;
}
while(!s.empty() && s.top()==0)s.pop();
if(s.empty()){
if(flag==1)s.push(2);
else s.push(1);
}
while(!s.empty()){
cout<<s.top();
s.pop();
}
cout<<endl;
}
return 0;
}
D 小A的线段(easy version)
思路:
因为 m ≤ 10 m\le 10 m≤10 很小,所以直接暴力枚举选取线段的情况,然后逐一验证即可。
验证的时候可以用差分来优化。
code:
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int maxn=15;
int n,m;
int st[maxn],ed[maxn];
bool check(int sta){
vector<int> a(n+5);
for(int i=0;i<m;i++){
if(sta>>i&1){
a[st[i]]++;
a[ed[i]+1]--;
}
}
for(int i=1;i<=n;i++){
a[i]+=a[i-1];
if(a[i]<2)return false;
}
return true;
}
int main(){
cin>>n>>m;
for(int i=0;i<m;i++)cin>>st[i]>>ed[i];
int ans=0;
for(int i=0;i<(1<<m);i++)
ans+=check(i);
cout<<ans;
return 0;
}
E 小A的任务
思路:
因为我们选了所有前 i i i 个 A A A 任务,才有资格去前 i i i 个 B B B 任务中选任务,一开始的想法是 d p dp dp。第 i i i 个任务我们可以只选一个 A A A 任务,或者同时选择一个 A A A 任务和相应的 B B B 任务,设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 个任务选 j j j 个 B B B 任务的最小时间,转移方程为 d p [ i ] [ j ] = m i n { d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − 1 ] + B i } + A i dp[i][j]=min\{dp[i-1][j],dp[i-1][j-1]+B_i\}+A_i dp[i][j]=min{dp[i−1][j],dp[i−1][j−1]+Bi}+Ai。不过这样转移时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的,因为它会同时处理出前 i i i 个任务中选择 0 ∼ i 0\sim i 0∼i 个 B B B 任务的最小代价,因此时间复杂度降不下来,大部分数据我们根本用不到。
考虑其他做法,发现如果我们决定只做前 i i i 个 A A A 任务后,我们做 k k k 个 B B B 任务一定是做前 i i i 个中用时最小的 k k k 个任务。这么做的好处是我们可以使用一个堆来动态维护住前 i i i 个 B B B 任务中最小的 k k k 个用时,这样我们跑一遍下来就得到了 i i i 取 k ∼ n k\sim n k∼n 所有情况下 B B B 任务的选取情况,也就得到了最小的用时。验证一遍的时间复杂度是 O ( n ∗ l o g k ) O(n*logk) O(n∗logk) 的,总时间复杂度应该是 O ( n ∗ q ∗ l o g k ) O(n*q*logk) O(n∗q∗logk),运算次数约为 1 0 5 ∗ 100 ∗ l o g ≈ 2 ∗ 1 0 8 10^5*100*log\approx2*10^8 105∗100∗log≈2∗108,时限有 3 s 3s 3s ,可以通过。
code:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
int n,q;
int a[maxn],b[maxn];
ll solve(int k){
ll tot=0,ans;
priority_queue<int> h;
for(int i=1;i<=k;i++){
tot+=a[i];
tot+=b[i];
h.push(b[i]);
}
ans=tot;
for(int i=k+1;i<=n;i++){
tot+=a[i];
if(b[i]<h.top()){
tot-=h.top();
h.pop();
tot+=b[i];
h.push(b[i]);
ans=min(ans,tot);
}
}
return ans;
}
int main(){
cin.tie(0)->sync_with_stdio(false);
cin>>n>>q;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
while(q--){
int k;
cin>>k;
cout<<solve(k)<<endl;
}
return 0;
}
F 小A的线段(hard version)
思路:
不难发现其实区间里很多位置其实都是没啥用的。 1 0 5 10^5 105 个位置被 200 200 200 个线段分成了 400 400 400 多个段,段内的所有位置其实没啥用,我们其实可以直接把这些段看成一个点,一个位置。这样离散化一下, 1 0 5 10^5 105 个位置就变成了 400 400 400 多个位置。
因为每个位置只要覆盖两次及以上就行了,所以每个位置我们可以看作只有三种状态:覆盖了零次,覆盖了一次,覆盖两次以上,每条线段可以将若干位置的状态进行变化,因此考虑动态规划。
设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 个位置覆盖了两次及以上,前 j j j 个位置覆盖了一次的选取线段方法数,为了前面状态不会出现断层(比如前面先覆盖了两次,然后后面变成覆盖一次,然后又变成覆盖了两次),我们对所有线段按左端点进行排序,然后按顺序取更新 d p dp dp 值即可(因为我们考虑了左端点在前 i i i 个位置的线段,那么前面的位置就必须都是覆盖了两次,否则后面更新 d p dp dp 也不可能更新状态,这样就保证了我们不需要考虑前面出现断层的情况,因为一定更新不了答案)。
官方题解给出的状态转移方程如下:
我写的时候没看题解,直接分类讨论的,思路是一样的。转移方程如下:
假设我们现在考虑到了第 k k k 个线段,线段左右端点分别为 l , r l,r l,r,设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 个位置覆盖了两次及以上,前 j j j 个位置覆盖了一次的选取线段方法数 ( l ≤ r , i ≤ j ) (l\le r,i\le j) (l≤r,i≤j)。于是有(箭头表示可以转移):
- d p [ k − 1 ] [ i ] [ j ] → d p [ k ] [ i ] [ j ] dp[k-1][i][j]\rightarrow dp[k][i][j] dp[k−1][i][j]→dp[k][i][j]
- 当 l − 1 ≤ i ≤ j ≤ r l-1\le i\le j\le r l−1≤i≤j≤r 时, d p [ k − 1 ] [ i ] [ j ] → d p [ k ] [ j ] [ r ] dp[k-1][i][j]\rightarrow dp[k][j][r] dp[k−1][i][j]→dp[k][j][r]
- 当 l − 1 ≤ i ≤ r ≤ j l-1\le i\le r\le j l−1≤i≤r≤j 时, d p [ k − 1 ] [ i ] [ j ] → d p [ k ] [ r ] [ j ] dp[k-1][i][j]\rightarrow dp[k][r][j] dp[k−1][i][j]→dp[k][r][j]
- 当 r < i ≤ j r\lt i\le j r<i≤j 时, d p [ k − 1 ] [ i ] [ j ] → d p [ k ] [ i ] [ j ] dp[k-1][i][j]\rightarrow dp[k][i][j] dp[k−1][i][j]→dp[k][i][j]
根据递推式,发现我们实际用的其实是 l − 1 l-1 l−1,而不是 l l l,因为离散化后点与点之间相邻关系会被打破,比如 3 , 7 3,7 3,7 离散化后是 1 , 2 1,2 1,2,虽然离散化后 2 2 2 减一就等于 1 1 1,但这并不代表 7 7 7 减一就等于 3 3 3,所以我们离散化的时候直接对 l − 1 l-1 l−1 离散化,而不是 l l l。
另外就是 1 1 1 和 n n n 作为区间左右端点也要放入到离散化数组中去,左端点由于上面所有线段左端点都减一的影响,这里也要减 1 1 1,也就是说认为左端点是 0 0 0。
code:
还能滚动数组优化,不过没写
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#define pii pair<int,int>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int maxm=205;
const ll mod=998244353;
int n,m;
vector<pii> itv;
vector<int> tmp;
int main(){
cin>>n>>m;
tmp.push_back(0);
tmp.push_back(n);
for(int i=1,l,r;i<=m;i++){
cin>>l>>r;l--;
tmp.push_back(l);
tmp.push_back(r);
itv.push_back(pii(l,r));
}
sort(tmp.begin(),tmp.end());
tmp.erase(unique(tmp.begin(),tmp.end()),tmp.end());
n=tmp.size();
auto find=[&](int x)->int{return lower_bound(tmp.begin(),tmp.end(),x)-tmp.begin()+1;};
for(auto& [l,r]:itv){
l=find(l);
r=find(r);
}
sort(itv.begin(),itv.end());
vector<vector<vector<ll> > > dp(m+1,vector<vector<ll> >(n+1,vector<ll>(n+1,0)));
dp[0][1][1]=1;
for(int k=1;k<=m;k++){
auto [l,r]=itv[k-1];
for(int i=0;i<=n;i++)
for(int j=i;j<=n;j++)
dp[k][i][j]=dp[k-1][i][j];
for(int i=0;i<=n;i++){
for(int j=i;j<=n;j++){
if(l<=i){
if(i<=r){
if(j<=r){
dp[k][j][r]+=dp[k-1][i][j];
dp[k][j][r]%=mod;
}
else {
dp[k][r][j]+=dp[k-1][i][j];
dp[k][r][j]%=mod;
}
}
else {
dp[k][i][j]+=dp[k-1][i][j];
dp[k][i][j]%=mod;
}
}
}
}
}
cout<<dp[m][n][n]<<endl;
return 0;
}