2023牛客暑期多校训练营1

目录

D.Chocolate

K.Subdivision

J.Roulette

H.Matches


D.Chocolate

题意:给定一个n*m的棋盘,每次可以选择一个点,标记该点左上区域的所有点,已经被标记的点不能被再被算入标记,每次必须标记一个点以上,最终不能操作的人获胜。

结论:只有当规格为1*1时,后手必胜,其余情况均是先手必胜。

思路:对于一个n*m的棋盘,先手每次都标记(n-1)*(m-1)而剩下的无论后手如何拿,先手都有办法只剩下一个空格给后手。

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'

using namespace std;

typedef pair<int, int> PII;
typedef long long ll;

int main()
{
	IOS
	ll n, m;
	cin >> n >> m;
	if(n == 1 && m == 1)
	{
		cout << "Walk Alone";
	}
	else cout << "Kelin";
	
	return 0;
}

K.Subdivision

题意:给定一个图,边权都为1,每次可以选择在一条边中添加一个点,保证点与点之间的边权始终为1,可以进行任意次数的操作,问最后到1距离小于等于k的点最多有多少个。

思路:贪心,显而易见,我们在越靠近1的点所在的边上去加点,越不划算,举个例子,我们在1-2中间添加一个点,而2后面连接了三个点,若k等于2,那么此时答案由原来的4变成了3。所以此题我们需要考虑的是如何加点才能使价值最大化,手玩第二组样例我们可以发现:

1.若一个点u的周围的点v不是由当前点更新而来,那么对于u-v这条边我们可以一直加点,不会对后续产生影响。

2.若一个点u周围的点v为u的前驱节点,那么对于u-v这条路径我们不能够进行加点。

3.若当前点u无后继节点且d[u]<=k,那么我们同理也可以继续加点。

总而言之,我们看当前是否能够进行加点的规则为:是否对周围节点产生影响。

#include<bits/stdc++.h>

using namespace std;
const int N=3e5+5;
typedef long long ll;
typedef pair<ll,ll> pll;
int mod=1e9+7;
const int maxv=4e6+5;

ll n,m,k;
vector<int> e[N];
void add(int u,int v)
{
    e[u].push_back(v);
    e[v].push_back(u);
}
ll d[N],st[N];
ll res=0;
int p[N];//记录节点的前驱节点
void solve()
{	
    cin>>n>>m>>k;
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        add(u,v);
    }
    queue<int> q;
    memset(d,0x3f,sizeof d);
    d[1]=0;
    q.push(1);
    st[1]=1;
    for(int i=1;i<=n;i++) p[i]=i;
    while(!q.empty()){//先跑一遍bfs
        int t=q.front();
        q.pop();
        for(auto x: e[t]){
            if(!st[x]){
                st[x]=1;
                d[x]=d[t]+1;
                p[x]=t;
                q.push(x);
            }
        }
    }
    for(int i=1;i<=n;i++){//当前节点的距离小于k直接算入答案中
        if(d[i]<=k){
            res++;
        }
    }
    for(int i=2;i<=n;i++){//直接遍历计算即可
        if(e[i].size()==1) res+=max(k-d[i],0ll);//因为是无向图,所以size为1代表无后继节点
        for(auto x: e[i]){
            if(p[x]!=i&&p[i]!=x){//如果当前这个点即不是前驱也是不后继
                res+=max(k-d[i],0ll);
            }
        }
    }
    cout<<res<<endl;

}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	//cin>>t;
	while(t--){
		solve();
	}
	system("pause");
	return 0;
}

J.Roulette

我们会发现,只要赢一次,就会赚一块钱,而无论前面输多少把(在保证本金的前提下),只要赢一把,同样也是赚一块钱。所以对于本金x,我们考虑最多能输的局数r,我们输1,2,4,8,……等等,由此,满足2^{r}-1<=x, 由此可以知,我们一直输的概率为(\frac{1}{2})^{r},那么赢的概率为:1-(\frac{1}{2})^{r},而由上式可知,我们每一个r,都会对应x所处的一个范围,例如当r=1时,x=1,r=2时,x=3,那么在[1,2],这个所处的范围,我们赢的概率是相同的,都是1/2,那么由此可以推得,我们的x在一段确定的区间中的概率相同,而我们最终所求的概率即为所有概率相乘。

我们还可以发现,r其实是一个比较小的数,为log级别,显然,我们是不能去暴力枚举每一个点的概率然后相乘,正因为r是log级别的数字,我们可以直接求出一段区间的概率总和,然后将每段区间的概率相乘即可。

而题目给出的形式是让我们求a,而a正是 \frac{x}{y} 在mod m下的逆元。所以我们直接进行逆元的相关计算即可。

#include<bits/stdc++.h>

using namespace std;
const int N=5e5+5;
typedef long long ll;
typedef pair<ll,ll> pll;
int mod=998244353;
const int maxv=4e6+5;

ll qmi(ll a,ll b)//快速幂
{
    ll res=1;
    while(b){
        if(b%2) res=res*a%mod;
        b>>=1;
        a=a*a%mod;
    }
    return res;
}

ll inv(ll x)//求逆元
{
    return qmi(x,mod-2);
}
void solve()
{	
    ll n,m;
    cin>>n>>m;
    ll ans =1;
    for(int i=n;i<n+m;){
        ll r=__lg(i+1);//枚举每个r
        ll end=min(n+m,(1ll<<(r+1))-1);//i为区间的起点,end为 区间终点
        ll len=end-i;//len为区间中点的个数
        ll v=(1-inv(qmi(2,r))+mod)%mod; //v即是求的1-(1/2)^r,因为除法取模会造成精度丢失,所以我们求出(1/2)^r的逆元,然后用1去减即可,因为不知道求出来逆元的大小,所以需要加上mod保证最后出来为正数
        ans=ans*qmi(v,len)%mod;//qmi(v,len)即是求的区间概率
        i=end;
    }
    cout<<ans%mod<<endl;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	//cin>>t;
	while(t--){
		solve();
	}
	system("pause");
	return 0;
}

H.Matches

题意:给定两个数组a和b,可以操作一次,交换其中一个数组里面的任意两个值,操作后让a数组和b数组的每一项差的绝对值相加的和最小。

我们会发现,当正序和反序相交的时候,进行交换,交换后我们可以减少交集区间的两遍,所以这题就转化为了给定两个集合,每个集合有若干个区间,从两个集合中各选一个区间,令这两个区间的交集最大。

将题意抽象成了上述形式,那么考虑如何进行维护。考虑最朴素的做法n*n,即两个集合进行一一枚举,肯定会超时,所以考虑减少时间复杂度,我们可以将两个集合混合,但仍然给原属于的集合打上标记,我们分别进行两遍枚举,即枚举正序和反序,那枚举正序进行举例,我们不必说每一个正序都去对反序进行一一枚举,我们可以按照左端点进行排序,然后若当前枚举的区间为反序,我们使用res去记录目前反序区间的最大右端点。

这样枚举的意义在于,因为是按照左端点进行的排序,所以res记录的反序区间的左端点肯定已经被我们枚举完了,那么对于反序区间的右端点,我们只用和当前枚举的正序端点取一个最小值然后减去当前正序左端点即为交集区间。

对于反序去枚举正序同理。

#include<bits/stdc++.h>

using namespace std;
const int N=5e6+5;
typedef long long ll;
typedef pair<ll,ll> pll;
int mod=998244353;
const int maxv=4e6+5;

ll qmi(int a,int b)
{
    ll res=1;
    while(b){
        if(b%2) res*=b%mod;
        b>>=1;
        a=a*a%mod;
    }
    return res;
}

ll inv(int x)
{
    return qmi(x,mod-2);
}

int n;

struct node
{
    int l,r,t;
};


void solve()
{	
    cin>>n;
    vector<int> a(n);
    vector<int> b(n);
    for(auto &x: a) cin>>x;
    for(auto &x: b) cin>>x;
    ll sum=0;
    for(int i=0;i<n;i++){
        sum+=abs(a[i]-b[i]);
    }
    vector<node> s;
    for(int i=0;i<n;i++){
        if(a[i]>b[i]){
            s.push_back({b[i],a[i],0});
        }
        else if(a[i]<b[i]){
            s.push_back({a[i],b[i],1});
        }
    }
    sort(s.begin(),s.end(),[](node x,node y){
        return x.l<y.l;
    });
    ll mx=0;
    ll res=-2e9;
    for(int i=0;i<s.size();i++){
        ll l,r,t;
        l=s[i].l,r=s[i].r,t=s[i].t;
        if(t==0){
            if(r>res){
                res=r;
            }
        }
        else{
            mx=max(mx,min(res,r)-l);
        }
    }
    res=-2e9;
    for(int i=0;i<s.size();i++){
        ll l,r,t;
        l=s[i].l,r=s[i].r,t=s[i].t;
        if(t==1){
            if(r>res){
                res=r;
            }
        }
        else{
            mx=max(mx,min(res,r)-l);
        }
    }
    cout<<sum-2*mx<<endl;


}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	//cin>>t;
	while(t--){
		solve();
	}
	system("pause");
	return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值