1.29寒假训练(2023-2024 Russia Team Open, High School Programming Contest (VKOSHP XXIV) 题解

1.29寒假训练

寒假的第一场训练,共13题

2023-2024 Russia Team Open, High School Programming Contest (VKOSHP XXIV).

可做题很多,差不多11道,最后写了9道,剩了一个K分类讨论题不想码和L题懒得写维护

在这里插入图片描述

A(签到)

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

string str;
map<char,int> mp,tim;
int n;
void work()
{
    cin>>str;
    cin>>n;
    rep(i,1,n)
    {
        char ch;
        int x;
        cin>>ch>>x;
        mp[ch]=x;
    }
    rep(i,0,sz(str)-1)
        tim[str[i]]++;
    int ans=0;
    for(char ch='a';ch<='z';ch++)
    {
        if(!mp[ch]) ans+=tim[ch];
        else
        {
            int k=tim[ch]%(mp[ch]-1);
            ans+=(tim[ch]/(mp[ch]-1))*mp[ch]+k+(k?1:0);
        }
    }
    cout<<ans<<'\n';
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

B(中等)

给定一棵 n n n个点的有根有向树,初始时在根节点有B,R两个木片

每次操作将 B 移动到原先节点的子节点,若 B 无法移动,游戏结束;
否则将 R 也移动到其原先节点的子节点,若 R 无法移动,则增加一枚 R 到此时 B 位置。

问游戏结束时最多能同时存在多少 R 木片

n ≤ 2 e 5 n \le 2e5 n2e5

思路:

从根节点开始思考这个问题是十分困难的,而因为R每次新增时,都恰好在当前B的位置,这很难不让我们往dp方向思考

d p [ u ] dp[u] dp[u]表示从 u u u开始游戏的最大答案,那么稍微思考即可写出如下转移方程

d p [ u ] = m a x ( d p [ x ] + 1 ) dp[u]=max(dp[x]+1) dp[u]=max(dp[x]+1),其中 x x x u u u子树中恰好存在叶子结点到u的路径长度等于 u − > x u->x u>x的路径长度的结点

画几棵树或者想办法来卡可以发现,这个转移方程的是可以被卡到 o ( n 2 ) o(n^2) o(n2)的,只要叶子结点的深度均匀分布,那么每个x都会被判断 n n n

考虑优化

容易发现的点是,祖先的dp值一定比子孙的大,也就是说,对于 u u u,继承与 u u u更近的 d p dp dp值一定是更优的。于是,最近的能更新的 d p dp dp值,取决于 u u u子树上深度最小的叶子结点。设为 t t t,我们只需要考虑 u u u子树上所有 d e p [ t ] = = d e p [ x ] dep[t]==dep[x] dep[t]==dep[x]的节点 x x x

倒推时进一步发现规律,对于某个非叶子节点,只要存在一个叶子节点与它深度相同,那么他对更高节点的贡献一定是它自己的 d p dp dp+1,而叶子节点的dp值一定是1,所以只需要写一个递归累加即可,不需要像官方题解那样使用二分或者线段树

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

const int maxn=5e5+5;

struct Edge
{
    int next;int to;
}edge[maxn];
int head[maxn],cnt,out[maxn],n;
void addedge(int from,int to)
{
    edge[++cnt].next=head[from];
    edge[cnt].to=to;
    head[from]=cnt;
}

int dep[maxn];
void dfs(int u)
{
    //dep[u]表示u到最近叶子结点的距离
    dep[u]=(out[u]==0?0:maxn);
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        dfs(v);
        dep[u]=min(dep[u],dep[v]);
    }
    dep[u]=(out[u]==0?0:dep[u]+1);
}

int calc(int u,int dis)
{
    //若dis==1说明此节点与叶子结点直接相连,那么答案一定+1
    int res=0;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        res=max(res,calc(v,(dis==1?dep[u]:dis-1)));
    }
    res+=(dis==1?1:0);
    return res;
}
void work()
{
    cin>>n;
    rep(i,2,n)
    {
        int x;
        cin>>x;
        addedge(x,i);
        out[x]++;
    }
    if(n==1) {cout<<"1\n";return;}
    dfs(1);
    cout<<calc(1,1)-1<<'\n';
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

D(简单)

题意:定义满足如下限制的树为圣诞树

1.深度为 n n n的节点恰好有 n n n

2.每个节点拥有最多不超过两个儿子

3.每个节点有标号,同一层从左到右依次递增标号,并且对于 u < v u<v u<v u u u所有儿子的标号必须小于 v v v

给定圣诞树的层数 n n n,问有多少种满足条件的圣诞树

思路:容易发现层与层之间是独立的,可以使用乘法原理

那么问题就转化为了计算 n − 1 n-1 n1个节点上挂 n n n条边有多少种挂法。

d p [ i ] [ j ] dp[i][j] dp[i][j] i i i个节点上挂 j j j条边的挂法

它可以由最后一个节点上挂零条,挂一条,挂两条转移而来,故转移方程为

d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j − 2 ] dp[i][j]=dp[i-1][j]+dp[i-1][j-1]+dp[i-1][j-2] dp[i][j]=dp[i1][j]+dp[i1][j1]+dp[i1][j2]

最后计算 ∏ 2 n d p [ n ] [ n − 1 ] \prod_{2}^{n}dp[n][n-1] 2ndp[n][n1]就是答案

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

const int mod=1e9+7;
const int maxn=5005;
int n;
ll dp[maxn][maxn];

void work()
{
    cin>>n;
    rep(i,0,n) dp[0][i]=1;

    dp[2][1]=1;
    rep(i,1,n)
        rep(j,1,n)
        {
            dp[i][j]=(dp[i-1][j-1]+dp[i][j-1])%mod;
            if(i>=2) dp[i][j]=(dp[i][j]+dp[i-2][j-1])%mod;
        }
    ll ans=1;
    rep(i,2,n)
        ans=(ans*dp[i][i-1])%mod;
    cout<<ans<<'\n';
}

int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

E (交互题,中等)

题意:未知进制 b a s e base base和初值 x x x,已知 x x x b a s e base base进制下的位数 n n n,每次询问可以输入 d d d,得到 x + d x+d x+d b a s e base base进制下有多少位,在 100 100 100次询问内求出 b a s e base base x x x的值

2 ≤ b a s e ≤ 2023 , 1 ≤ x ≤ 1 0 9 , 1 ≤ d ≤ 1 0 18 2\le base \le 2023,1\le x \le 10^9,1 \le d \le 10^{18} 2base2023,1x109,1d1018

思路:

先通过二分答案找到最小的 x + d 0 x+d_0 x+d0 x x x有不同位,此时有 b a s e n = x + d 0 base^n=x+d_0 basen=x+d0

再通过二分答案找到最小的 x + D + d 0 x+D+d_0 x+D+d0 x + d 0 x+d_0 x+d0有不同位,此时有 b a s e n + 1 = x + D + d 0 base^{n+1}=x+D+d_0 basen+1=x+D+d0

可通过这两个式子解出 b a s e base base d d d

由于 x x x最大为 1 0 9 10^9 109,考虑在不同进制下需要二分的上下界,我们知道 n n n p p p进制数最大为 p n − 1 p^n-1 pn1

发现如果进制过大,比如 b a s e > 1000 base > 1000 base>1000时, n ≤ 3 n\le 3 n3 此时 d 0 ≤ 202 3 3 = 8 , 279 , 186 , 167 d_0 \le 2023^3=8,279,186,167 d020233=8,279,186,167

如果进制较小,我们只需要取$x*base < 10^{12} $

因此第一次的上界取 1 0 12 10^{12} 1012,同理可以计算出第二次的上界 1 0 15 10^{15} 1015

询问次数 27 ( l o g 2 10 ) < 90 27(log_2^{10}) < 90 27(log210)<90

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

int st;
ll solve(ll l,ll r,int x)
{
    ll mid,ans;
    int res;
    while(l<=r)
    {
        mid=(l+r)>>1;
        printf("? %lld\n",mid);
        fflush(stdout);
        scanf("%d",&res);
        if(res>=st+x) r=mid-1,ans=mid;
        else l=mid+1;
    }
    return ans;
}
void work()
{
    scanf("%d",&st);
    ll ans1=solve(1,1e11,1),ans2=solve(1,5e15,2);
    ans2-=ans1;

    ll base;
    rep(i,2,2023)
    {
        ll tmp=ans2,cnt=0;
        while(tmp%i==0) tmp/=i,cnt++;
        if(tmp==i-1&&cnt==st) {base=i;break;}
    }
    ll ans=1;
    for(int i=1;i<=st;i++)
        ans=ans*base;
    ans-=ans1;
    printf("! %lld %lld\n",ans,base);
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

F(签到)

判断不正常的行和列,分情况讨论即可

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

const int maxn=1005;
ll sum;
int a[maxn][maxn];
int sumh[maxn],suml[maxn];
vector<int> l,h;
int n;

void work()
{
    cin>>n;
    sum=n*(n*n+1)/2;
    rep(i,1,n)
        rep(j,1,n)
            cin>>a[i][j],sumh[i]+=a[i][j];

    rep(i,1,n)
        rep(j,1,n)
            suml[i]+=a[j][i];
    rep(i,1,n)
        if(suml[i]!=sum) l.push_back(i);
    rep(i,1,n)
        if(sumh[i]!=sum) h.push_back(i);

    if(sz(l)==0||sz(h)==0)
    {
        if(sz(l)==0)
        {
            rep(i,1,n)
            {
                if(sumh[h[0]]-a[h[0]][i]+a[h[1]][i]==sum&&
                   sumh[h[1]]+a[h[0]][i]-a[h[1]][i]==sum)
                {cout<<h[0]<<' '<<i<<'\n'<<h[1]<<' '<<i<<'\n';return;}
            }
        }
        else
        {
            rep(i,1,n)
            {
                if(suml[l[0]]-a[i][l[0]]+a[i][l[1]]==sum&&
                   suml[l[1]]+a[i][l[0]]-a[i][l[1]]==sum)
                {cout<<i<<' '<<l[0]<<'\n'<<i<<' '<<l[1]<<'\n';return;}
            }
        }
    }
    else
    {
        if(suml[l[0]]-a[h[0]][l[0]]+a[h[1]][l[1]]==sum&&
           suml[l[1]]+a[h[0]][l[0]]-a[h[1]][l[1]]==sum&&
           sumh[h[0]]-a[h[0]][l[0]]+a[h[1]][l[1]]==sum&&
           sumh[h[1]]+a[h[0]][l[0]]-a[h[1]][l[1]]==sum
           )
            cout<<h[0]<<' '<<l[0]<<'\n'<<h[1]<<' '<<l[1]<<'\n';
        else
            cout<<h[0]<<' '<<l[1]<<'\n'<<h[1]<<' '<<l[0]<<'\n';
    }
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

G(简单)

题意:有 n n n个数,按某种次序累加到 s u m sum sum中,求有多少种次序,使得 s u m sum sum一直不是3的倍数

n ≤ 1 0 5 n \le 10^5 n105

思路:显然会想到将这 n n n个数对 3 3 3取模,若当前余数为2就只能加上2变成1,余数为1就只能加上1变成2

所以只能是 2 , 2 , 1 , 2 , 1 , 2 , 1 … … 2,2,1,2,1,2,1…… 2,2,1,2,1,2,1……或者 1 , 1 , 2 , 1 , 2 , 1 , 2 , 1 , 2 … … 1,1,2,1,2,1,2,1,2…… 1,1,2,1,2,1,2,1,2……这样的次序,余数为0不影响,可以随便填进去

设余数为 x x x的数的个数为 c n t x cnt_x cntx,由计数原理可以得出答案为

c n t 0 ! c n t 1 ! c n t 2 ! C n − 1 c n t 0 cnt_0!cnt_1!cnt_2! C^{cnt_0}_{n-1} cnt0!cnt1!cnt2!Cn1cnt0

只有当余数为1的个数和余数为2的个数的差的绝对值恰好为1时,答案不为0

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

const int maxn=1e5+5;
const int mod=1e9+7;
int n;
ll fact[maxn],infact[maxn];
map<int,int> mp;

ll quipow(int a,int b,int p)
{
	ll res=1;
	while(b)
	{
		if(b&1) res=(ll)res*a%p;
		b>>=1;a=(ll)a*a%p;
	}
	return res;
}

void init()
{
	fact[0]=infact[0]=1;
	for(int i=1;i<=maxn;i++)
	{
		fact[i]=((ll)fact[i-1]*i)%mod;
		infact[i]=((ll)infact[i-1]*quipow(i,mod-2,mod))%mod;
	}
}
int combi(int a,int b)
{
	return ((ll)((ll)fact[a]*infact[b])%mod*infact[a-b])%mod;
}

void work()
{
    cin>>n;
    rep(i,1,n)
    {
        int x;
        cin>>x;
        mp[x%3]++;
    }
    ll ans=(fact[mp[0]]*fact[mp[1]])%mod;
    ans=(ans*fact[mp[2]])%mod;
    ans=(ans*combi(n-1,mp[0]))%mod;
    if((mp[1]==mp[2])||(abs(mp[1]-mp[2])>2)) cout<<"0\n";
    else cout<<ans<<'\n';
}
int main()
{
    init();
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

H(中等)

给定一个自然数 n n n,设 n n n的一个划分为 a 1 + a 2 + … … a k = n a_1+a_2+……a_k=n a1+a2+……ak=n,求所有这样的划分的 m e x ( 0 , a 1 , a 2 … … a k ) mex(0,a_1,a_2……a_k) mex(0,a1,a2……ak)之和

n ≤ 1000 n\le 1000 n1000

思路:很容易想到的是分开计算每种 m e x mex mex值出现了多少次累加他们的贡献

那么对于 m e x ≥ x mex\ge x mexx的集合,我们知道肯定有 [ 1 , x − 1 ] [1,x-1] [1,x1]出现,等价于计算 n − x ( x − 1 ) 2 n-\frac {x(x-1)} {2} n2x(x1)且使用不超过 n n n的数的划分的数量

我们设 d [ i ] [ j ] d[i][j] d[i][j]表示将 i i i划分,且用到的最大数不超过 j j j的划分数量,那么根据用到 j j j和不用 j j j这两种决策有

d [ i ] [ j ] = d [ i − j ] [ j ] + d [ i ] [ j − 1 ] d[i][j]=d[i-j][j]+d[i][j-1] d[i][j]=d[ij][j]+d[i][j1]

那么对于 m e x = x mex=x mex=x的贡献,就是 x ∗ d [ n − x ( x − 1 ) 2 ] [ n ] − ( x + 1 ) ∗ d [ n − x ( x + 1 ) 2 ] [ n ] x*d[n-\frac {x(x-1)} {2}][n]-(x+1)*d[n-\frac {x(x+1)} {2}][n] xd[n2x(x1)][n](x+1)d[n2x(x+1)][n]

累加即可,当然,经过展开,或者集合划分的证明之后,我们可以得到答案就等于

∑ 1 i ( i − 1 ) 2 ≤ n d [ n − i ( i − 1 ) 2 ] [ n ] \sum ^{\frac {i(i-1)} {2} \le n}_{1} d[n-\frac {i(i-1)} {2}][n] 12i(i1)nd[n2i(i1)][n]

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

const int mod=1e9+7;
const int maxn=1005;

int n;
ll d[maxn][maxn];
void work()
{
    cin>>n;
    rep(i,0,n) d[0][i]=1;

    rep(i,1,n)
        rep(k,1,n)
        {
            d[i][k]=d[i][k-1];
            if(i>=k) d[i][k]=(d[i][k]+d[i-k][k])%mod;
        }
    ll ans=0;
    for(int i=1;i*(i-1)/2<=n;i++)
        ans=(ans+d[n-i*(i-1)/2][n])%mod;
    cout<<ans<<'\n';
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

I(诈骗题)

初始给定一个空集合 ,每次询问对集合操作一个 ( x , y ) (x,y) (x,y)的正方形,询问形如
( x , y ) (x,y) (x,y),对应以 ( x , y ) (x,y) (x,y)为左上角顶点的2*2正方形,对每次操作,若此正方形不在集合中,则加入集合,否则从该集合中删去

对于每次操作,输出集合中最大的不相互覆盖的正方形数量

其中 q ≤ 2 ∗ 1 0 5 q\le 2*10^5 q2105

题目的主要难度在于题意,以及发现规律:不相互覆盖的正方形的行号对2取模一定是相等的,剩下的事情就是计数了

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

map<int,int> mpx[2],mpy[2];
set<pair<int, int> > S;
int ans[4];

void work()
{
    int n;
    cin>>n;
    rep(i,1,n)
    {
        int x,y;
        cin>>x>>y;
        if(S.find({x,y})==S.end())
        {
            ans[x%2]-=max(mpx[0][x],mpx[1][x]);
            ans[y%2+2]-=max(mpy[0][y],mpy[1][y]);
            mpx[y%2][x]++;
            mpy[x%2][y]++;
            ans[x%2]+=max(mpx[0][x],mpx[1][x]);
            ans[y%2+2]+=max(mpy[0][y],mpy[1][y]);
            S.insert({x,y});
        }
        else
        {
            ans[x%2]-=max(mpx[0][x], mpx[1][x]);
            ans[y%2+2]-=max(mpy[0][y],mpy[1][y]);
            mpx[y%2][x]--;
            mpy[x%2][y]--;
            ans[x%2]+=max(mpx[0][x],mpx[1][x]);
            ans[y%2+2] += max(mpy[0][y],mpy[1][y]);
            S.erase({x, y});
        }
        cout<<max({ans[0],ans[1],ans[2],ans[3]})<<'\n';
    }
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

K(交互题,中等)

题意:有一串仅由 a , b , c a,b,c a,b,c组成的字符串,字符串的长度 n n n已知

每次询问形如:“? x ”ss“ “,返回第 x , x + 1 x,x+1 x,x+1位对应的字符数量 0 / 1 / 2 0/1/2 0/1/2

要求在 ⌈ 4 3 n ⌉ \lceil \frac 4 3 n\rceil 34n的询问次数内确定该字符串

题出的很好,启示我们在4次询问内确定三个连续的字符,发现其实随便试两种连续字符都能分类讨论

纯纯的分类讨论题,代码懒得写了

L(难,可补)

给定一个长为 S S S的只含a,b的字符串,要求支持如下两种操作

1.将某位的 a a a换成 b b b

2.询问某段区间是否能划分为 a , a b , b a a,ab,ba a,ab,ba这三种字符串

n ≤ 1 0 5 , q ≤ 1 0 5 n \le 10^5,q \le 10^5 n105,q105

M(签到)

#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define trav(a,x) for(auto&a : x)
#define all(x) x.begin(),x.end()
#define sz(x) (int) x.size()
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef long long ll;

int x,y,z,a,b,c;
const int inf=1e8;
int calc(int x)
{
    if(x<5) return a;
    if(5<=x&&x<10) return b;
    return c;
}
void work()
{
    int ans=inf;
    cin>>x>>y>>z;
    cin>>a>>b>>c;
    ans=min(ans,calc(x)+calc(y)+calc(z));
    ans=min(ans,calc(x+y)+calc(z));
    ans=min(ans,calc(x+z)+calc(y));
    ans=min(ans,calc(z+y)+calc(x));
    ans=min(ans,calc(x+y+z));
    cout<<ans<<'\n';
}
int main()
{
	cin.tie(0);
	cin.sync_with_stdio(0);
	int t=1;
	//cin>>t;
	while(t--)
		work();
	return 0;
}

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值