Skip to content

AGC001 做题记录

AGC001 做题记录

A - BBQ Easy

贪心得能取到的最大值为 \(L_1+L_3+L_5+\cdots+L_{2N-1}\) ,简单计算并输出即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
using namespace std;
bool cmp(int x,int y){return x<y;}
int main(){
    int n,a[205],ans=0;
    cin>>n;
    rep(i,1,2*n)cin>>a[i];
    sort(a+1,a+2*n+1,cmp);
    for(int i=1;i<=2*n;i+=2){
        ans+=a[i];
    }
    cout<<ans<<endl;
    return 0;
}

B - Mysterious Light

很有意思的数学题。首先发现光线在两次反射过后会进入一个边长为 \((x,y)\) \((x>y)\) 的平行四边形,再经过两次反射后 \((x-y,y)\) 。发现这个过程类似欧几里得算法,边长会进行辗转相除,所以我们可以考虑一个类似的递归函数计算答案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define ll long long
using namespace std;
inline ll read(){
    ll x=0,f=1;char ch=getchar();
    while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
    return x*f;
}
ll ans;
inline void dfs(ll x,ll y){
    if(x%y==0){
        ans+=x*2-y;
        return;
    }else{
        ans+=(x-x%y)*2;
        dfs(y,x%y);
    }
}
int main(){
    ll n=read(),x=read();
    ans=n;
    dfs(max(n-x,x),min(n-x,x));
    cout<<ans<<endl;
    return 0;
}

C - Shorten Diameter

又一道很不错的思维题。首先观察数据范围,发现时间复杂度大概为 \(O(N^2)\) 以下。考虑直径的几何定义(大草),我们枚举这个树的圆心(草*2),画一个半径为 \(K/2\) 的圆,并且把圆外的点删掉,判断删掉的点数的最小值。这样的做法是 \(O(N^2)\) 的。特别地,当 \(K\) 为奇数时我们把一条边当做圆心。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define ll long long
using namespace std;
inline ll read(){
    ll x=0,f=1;char ch=getchar();
    while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
    return x*f;
}
const int N=2e3+5;
vector<int> G[N];
int n,k,ans,tot;
inline void dfs(int u,int fa,int dep){
    ++tot;
    if(dep==0)return;
    for(int v:G[u]){
        if(v==fa)continue;
        dfs(v,u,dep-1);
    }
}
int main(){
    n=read(),k=read();
    rep(i,1,n-1){
        int u=read(),v=read();
        G[u].push_back(v);
        G[v].push_back(u);
    }
    if(k%2==0){
        rep(i,1,n){
            tot=0;dfs(i,0,k/2);
            ans=max(ans,tot);
        }
    }else{
        rep(i,1,n){
            for(int j:G[i]){
                tot=0;
                dfs(i,j,k/2);dfs(j,i,k/2);
                ans=max(ans,tot);
            }
        }
    }
    cout<<n-ans<<endl;
    return 0;
}

D - Arrays and Palindrome

很有MO风格的题目。一个 \([l,r]\) 之间的回文条件相当于给出了 \(s_l=s_r,s_{l+1}=s_{r-1},\dots\) 总共 \(\lfloor \dfrac{a_i}{2}\rfloor\) 个限制。把这些限制看作边,那么仅当所有点在同一个连通块里时每个字符必须相等,这意味着需要至少 \(n-1\) 条边。如果 \(a_i\) 中有三个奇数,那么会浪费掉 \(1.5\) 条边,显然不合法。

下面考虑构造。发现如果 \([1,n-1],[1,n]\) 都是回文串,那么所有元素相同。于是可以把 \(a\) 中的奇数放到头或尾,\(b\) 数组的头和尾分别 \(+1,-1\),其他元素和 \(a\) 数组相同。这样类似错位摆放的正确性是显然的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define ll long long
using namespace std;
inline ll read(){
    ll x=0,f=1;char ch=getchar();
    while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
    return x*f;
}
int n,m,a[105],b[105],tot,cnt[2];
bool cmp(int x,int y){return x%2>y%2;}
int main(){
    n=read(),m=read();
    rep(i,1,m){
        a[i]=read();
        ++cnt[a[i]&1];
    }
    if(m==1){
        if(a[1]==1)printf("1\n1\n1\n");
        else printf("%d\n%d\n%d %d\n",a[1],2,1,a[1]-1);
        return 0;
    }
    if(cnt[1]>2)return puts("Impossible"),0;
    sort(a+1,a+m+1,cmp);
    printf("%d ",a[1]);
    rep(i,3,m)printf("%d ",a[i]);
    printf("%d\n",a[2]);
    b[++tot]=a[1]+1;
    rep(i,3,m)b[++tot]=a[i];
    if(a[2]>1)b[++tot]=a[2]-1;
    printf("%d\n",tot);
    rep(i,1,tot)printf("%d ",b[i]);
    return 0;
}

E - BBQ Hard

\(O(N^2)\) 的暴力计算不是我们想要的。看到形如 的形式的数求和,想到经典的格路计数问题。这等价于从 \((0,0)\) 走到 \((x,y)\) 的方案数。于是这变成了求 \((0,0)\) 到所有 \((a_i+a_j,b_i+b_j)\) 的路径数之和。可以通过减少终点数来优化,平移路径到 \((-a_i,-b_i)\rightarrow(a_j,b_j)\) 。可以用简单的dp计算,最后去掉 \(i=j\) 时的答案即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define per(i,a,b) for(int i=(a);i>=(b);--i)
#define ll long long
using namespace std;
inline ll read(){
    ll x=0,f=1;char ch=getchar();
    while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
    return x*f;
}
const int N=2e5+5,S=2e3+5,P=1e9+7,inv2=5e8+4;
ll fpow(ll x,ll y){
    ll res=1;
    while(y){
        if(y&1)res=res*x%P;
        y>>=1;x=x*x%P;
    }
    return res;
}
int fac[8050],ifac[8050];
ll C(int n,int m){return 1ll*fac[n]*ifac[m]%P*ifac[n-m]%P;}
int n,a[N],b[N],dp[4050][4050];
ll ans;
int main(){
    n=read();
    rep(i,1,n){
        a[i]=read(),b[i]=read();
        ++dp[S-a[i]][S-b[i]];
    }
    fac[0]=1;
    rep(i,1,8000)fac[i]=1ll*fac[i-1]*i%P;
    ifac[8000]=fpow(fac[8000],P-2);
    per(i,7999,0)ifac[i]=1ll*ifac[i+1]*(i+1)%P;
    rep(i,1,S<<1)rep(j,1,S<<1){
        dp[i][j]=(dp[i][j]+(dp[i-1][j]+dp[i][j-1])%P)%P;
    }
    rep(i,1,n){
        ans=(ans+dp[S+a[i]][S+b[i]])%P;
        ans=(ans-C(2*a[i]+2*b[i],2*a[i]))%P;
    }
    ans=(ans+P)%P*inv2%P;
    cout<<ans<<endl;
    return 0;
}

F - Wide Swap

\(i,j\) 两个位置能交换的条件为 \(|i-j|\ge K\)\(|P_i-P_j|= 1\) ,这个条件太玄妙了,考虑构造排列 \(Q\) 满足 \(Q_{P_i}=i\) ,那么对于 \(Q\)\(|Q_i-Q_j|\ge K\)\(|i-j|=1\)\(i,j\) 相邻时,我们可以交换。而求出字典序最小的 \(P\) 也就等价于 优先考虑 \(1\)\(Q\) 上的下标最靠前,然后考虑 \(2\) 的下标,以此类推。

于是我们发现对于所有在 \(Q\) 中差值小于 \(K\) 的数对 \((u,v)\) ,它们无法交换,也就是相对位置被固定了。对于这样的数对 \((u,v)\) ,我们把在 \(Q\) 中出现较前的数向出现较后的数连一条有向边,不难发现这样建图会构成一张 \(DAG\)\(Q\) 可以转换成任意一个满足该图拓扑序的序列。

注意到 \(Q\) 的要求并不是简单的字典序最小,因此直接建图后贪心取最小值是错误的,正确的做法类似 [HNOI2015]菜肴制作 ,应当建反图后求反图中字典序最大的排列。最后还有一个问题,如果我们对所有 \(|u-v|< K\) 都连边,边数是 \(O(NK)\) 的,所以不能显式建图。

首先把 \(Q\) 上建反图换到原序列上,类似的,即对所有点 \(i\) 连上 \(\{j||i-j|<K,P_j<P_i\}\) ,若点 \(i\) 入度为 \(0\) ,可以发现在区间 \((i-K,i+K)\)\(P_i\) 最大。用大根堆维护入度为 \(0\) 的点集,线段树维护区间最值,当删除点 \(i\) 时,判断 \((i-K,i),(i,i+K)\) 两个区间的最大值中是否有入度变为 \(0\)

时间复杂度:\(O(N\log N)\)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define li (i<<1)
#define ri (i<<1|1)
#define mid (l+r>>1)
#define ls li,l,mid
#define rs ri,mid+1,r
#define ll long long
using namespace std;
inline ll read(){
    ll x=0,f=1;char ch=getchar();
    while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
    return x*f;
}
const int N=5e5+5,inf=0x3f3f3f3f,M=2e6+5;
int n,k,P[N],Ans[N],mx[M];
void upd(int i){mx[i]=P[mx[li]]>P[mx[ri]]?mx[li]:mx[ri];}
void Build(int i,int l,int r){
    if(l==r)return mx[i]=l,void();
    Build(ls),Build(rs);upd(i);
}
void del(int i,int l,int r,int p){
    if(l==r)return mx[i]=0,void();
    p<=mid?del(ls,p):del(rs,p);upd(i);
}
int Q(int i,int l,int r,int a,int b){
    if(r<a||b<l)return 0;
    if(a<=l&&r<=b)return mx[i];
    int v1=Q(ls,a,b),v2=Q(rs,a,b);
    return P[v1]>P[v2]?v1:v2;
}
bool vis[N];
priority_queue<int> pq;
inline void check(int id){
    if(vis[id])return;
    if(Q(1,1,n,id-k+1,id+k-1)==id){
        pq.push(id);vis[id]=1;
    }
}
int main(){
    n=read(),k=read();
    rep(i,1,n)P[i]=read();
    P[0]=-inf;
    Build(1,1,n);
    rep(i,1,n)check(i);
    for(int i=n;i>=1;i--){
        int u=pq.top();pq.pop();
        Ans[u]=i;del(1,1,n,u);
        int pos;
        if((pos=Q(1,1,n,u-k+1,u-1)))check(pos);
        if((pos=Q(1,1,n,u+1,u+k-1)))check(pos);
    }
    rep(i,1,n)printf("%d\n",Ans[i]);
    return 0;
}

Comments