郑州大学2024年3月天梯赛选拔赛题解(A-L)

终榜
题单

这场的题水平很高,由acm实验室队长亲自拉的题单(教练?什么教练?不认识),上面的题都是abc(atcoder beginer contest)原题,比赛结束后在每道题的名字上都列出了出处可以直接查到相关题目,oj以及题解

题目难度我觉得应该是: B , E , F < C , D , I < A , K < G , H , J < L B,E,F\lt C,D,I \lt A,K\lt G,H,J\lt L B,E,F<C,D,I<A,K<G,H,J<L

比赛链接(建议还是直接搜原题,在洛谷上有OJ和题解,郑州大学的系统需要校园网,而且本身也不是很好用)

不知不觉已经写了100篇了,小小纪念一下 。


A.博弈论 [ABC148F] Playing Tag on Tree

题面:

在这里插入图片描述

思路:

一开始比较直观的想法就是把妹妹的位置看作树根,邱宇向叶节点方向走,走到尽头被妹妹堵死的时候游戏结束。

不过在妹妹追上邱宇之前,邱宇还有时间可以往根走走,这样有可能会找到其他路线,使得能到达更深的位置,如下图:

请添加图片描述

因此我们邱宇哥哥的行走策略就确定了。先往根走,直到脸上就是妹妹(就是不能再向上走了,否则就撞上了),然后再沿着当前位置能到达最深位置的链走。

考虑向上走到什么时候:因为我们假定妹妹在树根,因此她的深度就是 0 0 0,如果邱宇的深度是 h t ht ht,那么邱宇可以向上走 ⌊ h t − 1 2 ⌋ \left\lfloor\dfrac{ht-1}2\right\rfloor 2ht1 步(举两个实例手玩一下就能推出来了)。

然后我们需要知道这个位置到最深层次的链的长度是多少,假如是 l e n len len,注意因为规则规定每个人必须走,而且邱宇先走,所以有可能邱宇走到尽头后,需要向回走(因为前面没路了),这时就可能撞到妹妹怀里,妹妹就没有必要走出她的一步了。不过无论哪种情况,妹妹都会走到叶子节点的父节点位置上。

这样的话,我们要知道妹妹走了几步,其实就是妹妹 走到邱宇的“折返”点到根的距离 加上 “折返”点到最深深度的距离-1。这就需要知道每个点的深度 h t ht ht 数组,每个点到达的最深深度的距离数组 l e n len len,另外为了向上找到折返点,还要维护每个节点的父亲节点数组 f a fa fa

我们先以妹妹所在点为树根,跑一遍dfs,处理好这些信息,然后找到折返点下标 i d x idx idx,答案就是 h t [ i d x ] + l e n [ i d x ] − 1 ht[idx]+len[idx]-1 ht[idx]+len[idx]1

另外还有一个想法,就是以妹妹和邱宇分别为起点跑两次单源最短路,可以得到两人到每个点的步数,妹妹的步数大于邱宇的前提下,点的妹妹的最大的步数-1就是答案。

这其实跟上面的思路差不多。妹妹和邱宇在到达这个点之前一定有一段路线是重复的,因为妹妹的步数大于邱宇,所以邱宇会先于妹妹进入这个路线,在走到尽头之前不会被妹妹抓住。之后就是上面的情形了。邱宇被妹妹壁咚,邱宇主动进攻,迎入妹妹的怀抱…咳咳,扯远了,所以妹妹会少走一步。

code:

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int maxn=1e5+5;
 
int n,x,y;
 
int head[maxn],cnt;
struct edge{
    int v,nxt;
}e[maxn<<1];
void add(int u,int v){
    e[++cnt].v=v;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
 
int fa[maxn],len[maxn],ht[maxn];
void dfs(int u,int rt,int h){
    for(int i=head[u],v;i;i=e[i].nxt){
        v=e[i].v;
        if(v==rt)continue;
        fa[v]=u;
        ht[v]=ht[u]+1;
        dfs(v,u,h+1);
        len[u]=max(len[u],len[v]+1);
    }
}
 
int main(){
    cin>>n>>x>>y;
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        add(u,v);
        add(v,u);
    }
    dfs(y,-1,0);
     
    int idx=x;
    for(int i=1;i<=(ht[x]-1)/2;i++)idx=fa[idx];
    cout<<ht[idx]+len[idx]-1;
    return 0;
}

B.分发礼物 [ABC148C] Snack

题面:

在这里插入图片描述

思路:

签到题,就是问一个最小的数,既可以整除 A A A,又可以整除 B B B。显然就是求 A A A B B B 的最小公倍数。

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
 
ll a,b;
ll gcd(ll a,ll b){
    while(b)b^=a^=b^=a%=b;
    return a;
}
 
int main(){
    cin>>a>>b;
    cout<<a*b/gcd(a,b);
    return 0;
}

C.石头剪刀布 [ABC149D] Prediction and Restriction

题面:

在这里插入图片描述

思路:

换句话说,就是你在第 i i i 局出了某个手势,那么第 i + k i+k i+k 局就不能出相同的手势。如果妹妹的第 i i i 局和第 i + k i+k i+k 局的手势不一样,那么对邱宇来说两局都可以赢,因为两局赢的手势不重复。但是如果两局妹妹手势一样,那么必须有一局平了或输了,因为输了没有惩罚,所以我们让这局平了或者输了都是可以的。

假如我们让第 i i i 局赢,第 i + k i+k i+k 局平或输。也就是说,在第 i + k i+k i+k 局上,邱宇可以出两种手势。这样的话,第 i + 2 ∗ k i+2*k i+2k 局就可以赢了,因为如果第 i + 2 ∗ k i+2*k i+2k 和第 i + k i+k i+k 局手势重复,那么第 i + k i+k i+k 局的手势就可以换一下,而且代价不变。

所以我们贪心地让邱宇赢,如果某一局与前面第 i − k i-k ik 局的胜利的手势重复,就把这局标记掉,不算分数。之后看到第 i + k i+k i+k 局的时候,就不认为它和第 i i i 局手势重复,让这局赢就行了。

code:

#include <iostream>
#include <cstdio>
#include  <algorithm>
using namespace std;
const int maxn=1e5+5;
 
int n,k,r,s,p;
string t;
 
int main(){
    cin>>n>>k>>r>>s>>p>>t;
    long long ans=0;
    for(int i=0;i<n;i++){
        if(i-k>=0 && t[i]==t[i-k])t[i]='!';
        else {
            if(t[i]=='r')ans+=p;
            else if(t[i]=='p')ans+=s;
            else ans+=r;
        }
    }
    cout<<ans<<endl;
    return 0;
}

D.异或和的和 [ABC147D] Xor Sum 4

题面:

在这里插入图片描述

思路:

二进制的题,算贡献一般都是要每个二进制位单独算贡献。

对二进制第 i i i 位(从第 0 0 0 位开始),假设有 c n t 0 cnt_0 cnt0 个数这一位上是 0 0 0,有 c n t 1 cnt_1 cnt1 个数这一位上是 1 1 1,那么这一位对答案的贡献就是 c n t 0 ∗ c n t 1 ∗ 2 i cnt_0*cnt_1*2^i cnt0cnt12i

上面那个式子的含义就是:异或后这一位上是 1 1 1 的对数乘上这一位上是 1 1 1 的贡献。而对数就是 取一个这一位上是 0 0 0 的数和取一个这一位上是 1 1 1 的数组成一对的个数。

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn=3e5+5;
const ll mod=1e9+7;
 
ll n,cnt[65];
 
int main(){
    cin>>n;
    for(ll i=1,t;i<=n;i++){
        cin>>t;
        for(ll j=0;j<60;j++){
            if((t>>j)&1)
                cnt[j]++;
        }
    }
    ll ans=0;
    for(ll i=0;i<60;i++){
        ans=(ans+(1ll<<i)%mod*cnt[i]%mod*(n-cnt[i]))%mod;
    }
    cout<<ans<<endl;
    return 0;
}

E.吃饼干 [ABC149B] Greedy Takahashi

题面:

在这里插入图片描述

思路:

签到题,就是先吃邱宇的,再吃自己的,没得吃了就返回。模拟过程或者分类讨论即可。

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
 
ll a,b,k;
 
int main(){
    cin>>a>>b>>k;
    if(k>a+b)cout<<"0 0";
    else if(k>a)cout<<0<<" "<<b-(k-a);
    else cout<<a-k<<" "<<b; 
    return 0;
}

F.abs(全排列-全排列) [ABC150C] Count Order

题面:

在这里插入图片描述

思路:

因为 n ≤ 8 n\le 8 n8,所以直接暴力枚举全排列。签到题。

找到下一个全排列可以使用 next_permutation() 函数。

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
 
int n,t1,t2;
vector<int> a;
 
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)a.push_back(i);
    vector<int> b(n),c(n);
    for(int i=0;i<n;i++)cin>>b[i];
    for(int i=0;i<n;i++)cin>>c[i];
     
    do{
        static int cnt=0;
        cnt++;
        if(a==b)t1=cnt;
        if(a==c)t2=cnt;
    }while(next_permutation(a.begin(),a.end()));
    cout<<abs(t1-t2);
    return 0;
}

G.重排 [ABC163E] Active Infants

题面:

在这里插入图片描述

思路:

一开始的想法是贪心,把最大的数放在最远的位置上,以此类推。但是这个想法假了,比如第二个样例, 6 , 5 , 5 6,5,5 6,5,5 依次放在右边, 1 , 1 , 1 1,1,1 1,1,1 放左边的答案是 57 57 57。但是 6 6 6 放在左边, 5 , 5 5,5 5,5 放右边, 1 , 1 , 1 1,1,1 1,1,1 放中间就是 58 58 58

不过这个贪心的过程还是有些参考价值的,也就是最大的数一定放在两头之一。不妨假设最大的数为 x x x,它放在最后面,它前面放的 y y y。如果这样不优 ,那么 y y y 放在后面的答案更优,这样的话,最大值距离减小 1 1 1,总答案损失 x x x y y y 的距离增加 1 1 1,总答案增加 y y y。不过因为 x x x 是最大值,因此答案会减少。肯定不优,与前面的假设矛盾。

所以我们对原本的数组进行从大到小排序,并尝试依次把当前的最大值放在两头中的一个,不过这样爆搜肯定会超时。考虑到我们放好前 i d x idx idx 个数后,剩下的区间为 [ l , r ] [l,r] [l,r],这个状态下的最优答案是固定的,也就可以记忆化搜索(其实这就是动态规划),这样时间复杂度最坏是 O ( n 2 ) O(n^2) O(n2) 的。

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll ;
const int maxn=2e3+5;

int n;
pair<int,int> a[maxn];
ll dp[maxn][maxn];

ll dfs(int idx,int l,int r){
	if(idx>n || l>r)return 0;
	if(dp[l][r])return dp[l][r];
	ll x=a[idx].first,y=a[idx].second;
	ll a1=dfs(idx+1,l,r-1)+x*abs(r-y),a2=dfs(idx+1,l+1,r)+x*abs(l-y);
	return dp[l][r]=max(a1,a2);
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i].first,a[i].second=i;
	sort(a+1,a+n+1,greater<pair<int,int> >());
	cout<<dfs(1,1,n);
	return 0;
}

H.期望 [ABC149F] Surrounded Nodes

题面:

在这里插入图片描述

思路:

看不懂题面。其实就是涂好色之后,选定最小的一个子树,使得这个子树包含所有黑色点,问选上白色点的个数的期望是多少。

每个点都等概率被涂成黑色或者白色,所以所有 n n n 个点一共就 2 n 2^n 2n 种涂色可能,每种可能的概率都是 1 2 n \dfrac1{2^n} 2n1

发现对一个子树求期望很困难,因此尝试对每个点单独算各自的期望。假如一个点 u u u 的贡献为 a n s u ans_u ansu,和他直接相连的点的集合为 S o n Son Son,以某个点 v v v 为根的子树的子树大小为 s z v sz_v szv。那么一个点的贡献就是:这个点为白色点,并且这个点一定被包含进子树的情况的个数除以总的情况个数。

总的情况个数是 2 n 2^n 2n。这个点 u u u 为白色点,其它点没有限制的总的情况个数为 2 n − 1 2^{n-1} 2n1。考虑什么情况下会包含到这个白色点 u u u发现当点 u u u 的子树中有两个及以上包含黑色点时候一定会包含它,因为两边都要包含到,中间的过渡点也一定得包含到。如果直接去算会比较困难,但是如果用总个数减去没有子树包含黑色点的个数以及只有一个子树包含黑色点的个数就很好算。列式如下: a n s u = 2 n − 1 − 1 − ∑ v ∈ S o n ( 2 s z v − 1 ) 2 n ans_u=\dfrac{2^{n-1}-1-\sum_{v\in Son}(2^{sz_v}-1)}{2^n} ansu=2n2n11vSon(2szv1)

我们只需要先跑一边dfs ,把 s z sz sz 数组算出来,然后再dfs一次,把每个点的贡献算出来并累加进答案即可。注意我们把这个无根树堪称有根树来dfs的时候,节点 u u u 的父亲节点也得算作 u u u 的一颗子树,这颗子树的大小就是 s z f a = n − s z u sz_{fa}=n-sz_u szfa=nszu

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
const ll mod=1e9+7;

int n,sz[maxn];

int head[maxn],cnt;
struct edge{
	int v,nxt;
}e[maxn<<1];
void add(int u,int v){
	e[++cnt].v=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}

ll qpow(ll a,ll b){
	b%=mod-1;
	ll base=a%mod,ans=1;
	while(b){
		if(b&1){
			ans*=base;
			ans%=mod;
		}
		base*=base;
		base%=mod;
		b>>=1;
	}
	return ans;
}
ll inv(ll x){return qpow(x,mod-2);}

void dfs1(int u,int rt){
	sz[u]=1;
	for(int i=head[u],v;i;i=e[i].nxt){
		v=e[i].v;
		if(v==rt)continue;
		dfs1(v,u);
		sz[u]+=sz[v];
	}
}
ll ans=0;
void dfs(int u,int rt){
	if(sz[u]==1)return;//可写可不写
	ll ansu=qpow(2,n-1)-1;
	for(int i=head[u],v,szv;i;i=e[i].nxt){
		v=e[i].v;
		if(v==rt)szv=n-sz[u];
		else szv=sz[v];
		ansu=(ansu-(qpow(2,szv)-1))%mod;
		if(v!=rt)dfs(v,u);
	}
	ans=(ans+ansu*inv(qpow(2,n)))%mod;
}

int main(){
	cin>>n;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs1(1,-1);
	
	dfs(1,-1);
	cout<<(ans+mod)%mod;
	return 0;
}

I.删数字 [ABC148D] Brick Break

题面:

在这里插入图片描述

思路:

贪心即可 ,因为我们删掉数字的时候不能改变相对顺序,所以只能先搞出前 i − 1 i-1 i1 个数,然后再靠删掉 i − 1 i-1 i1 这个数后面的一些数,来把 i i i 放到正确的位置上。我们贪心地留第一个遇到的 i i i,这样可以给后面的删数操作留出更多数,操作空间更大 。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=2e5+5;
 
int n,cnt;
 
int main(){
    cin>>n;
    for(int i=1,t;i<=n;i++){
        cin>>t;
        if(t==cnt+1)cnt++;
    }
    cout<<((cnt)?n-cnt:-1);
    return 0;
}

J.网格 [ABC147E] Balanced Path

题面:

在这里插入图片描述

思路:

n , m ≤ 80 n,m\le 80 n,m80。。。没看错的话,这应该是个 O ( n 4 ) O(n^4) O(n4) 的做法。

求路径上的红色数字之和减去黑色数字之和,也就是每个位置上红色和黑色之差的和,那么我们可以稍微简化一下,每个位置直接两个数字相减,每次涂色就变成了取这个差值的正值还是取负值了。题面就变成了一路取下来的差值之和的绝对值最小。

这就有点背包的味道了,当我们从某个位置到达下一个位置的时候,要么拿正值,要么拿负值。不过因为会拿负值,下标有可能是负数,所以我们需要引入偏移量,给下标加上一个比较大的偏移量,就不会出现负数下标了。

因为 0 ≤ A , B ≤ 80 0\le A,B\le 80 0A,B80,所以差值的范围是 [ − 80 , 80 ] [-80,80] [80,80],一路走下来的话最多经过 n + m − 1 = 159 n+m-1=159 n+m1=159 个点,值最大是 159 ∗ 80 159*80 15980,不过当这个值超过 80 ∗ 80 80*80 8080 的时候肯定不优,因为我们前一半取正后一半取负肯定更好,全取正更新不了最优答案,所以我们值域开到 − 80 ∗ 80 ∼ 80 ∗ 80 -80*80\sim80*80 80808080 就行了,对应的偏移量 o f f s offs offs 就是 80 ∗ 80 80*80 8080

a [ i ] [ j ] a[i][j] a[i][j] 表示坐标 ( i , j ) (i,j) (i,j) 上的差值, d p [ i ] [ j ] [ i d x ] dp[i][j][idx] dp[i][j][idx] 表示坐标 ( i , j ) (i,j) (i,j) 能否取到 i d x idx idx。那么有 d p [ i ] [ j ] [ i d x ] = d p [ i − 1 ] [ j ] [ i d x + a [ i ] [ j ] ]   ∣   d p [ i − 1 ] [ j ] [ i d x − a [ i ] [ j ] ]   ∣   d p [ i ] [ j − 1 ] [ i d x + a [ i ] [ j ] ]   ∣   d p [ i ] [ j − 1 ] [ i d x − a [ i ] [ j ] ] dp[i][j][idx]=dp[i-1][j][idx+a[i][j]]\,|\,dp[i-1][j][idx-a[i][j]]\,|\,\\dp[i][j-1][idx+a[i][j]]\,|\,dp[i][j-1][idx-a[i][j]] dp[i][j][idx]=dp[i1][j][idx+a[i][j]]dp[i1][j][idxa[i][j]]dp[i][j1][idx+a[i][j]]dp[i][j1][idxa[i][j]]记得下标加上偏移量。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=85;
const int offs=80*80;
 
int n,m;
int a[maxn][maxn];
bool dp[maxn][maxn][maxn*maxn*2];
 
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
    for(int i=1,t;i<=n;i++)
        for(int j=1;j<=m;j++){
            cin>>t;
            a[i][j]=abs(a[i][j]-t);
        }
     
    dp[1][1][offs+a[1][1]]=dp[1][1][offs-a[1][1]]=true;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=-offs;k<=offs;k++){
                if(i>1){
                    if(k-a[i-1][j]>=-offs)dp[i][j][offs+k]|=dp[i-1][j][offs+k-a[i][j]];
                    if(k+a[i-1][j]<=offs)dp[i][j][offs+k]|=dp[i-1][j][offs+k+a[i][j]];
                }
                if(j>1){
                    if(k-a[i][j-1]>=-offs)dp[i][j][offs+k]|=dp[i][j-1][offs+k-a[i][j]];
                    if(k+a[i][j-1]<=offs)dp[i][j][offs+k]|=dp[i][j-1][offs+k+a[i][j]];
                }
            }
        }
    }
     
    for(int i=0;i<=offs;i++){
        if(dp[n][m][offs+i]){
            cout<<i<<endl;
            return 0;
        }
    }
    return 0;
}

K.个数是和的余数 [ABC146E] Rem of Sum is Num

题面:

在这里插入图片描述

思路:

区间长度等于区间和模 k k k 的余数。这个性质光一看就觉得很不好维护,考虑转化。我们给每个元素减去一个 1 1 1,这样就转化成了区间和模 k k k 等于 0 0 0(也就是区间和是 k k k 的倍数)。

转化成这个就好说了,我们一边处理前缀和,一边以当前位置 i i i 为区间右端点,看有没有前缀和相等的左端点,把满足条件的左端点个数累加起来就行了。

不过发现假了。进一步考虑发现,这个区间的长度不应该大于等于 k k k ,因为这时区间长度大于等于 k k k,而模 k k k 的余数却是小于 k k k 的。考虑用滑动窗口的思想,用队列优化一下就可以了 。

code:

#include <iostream>
#include <cstdio>
#include <map>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
 
ll n,k;
ll s[maxn];
 
int main(){
    cin>>n>>k;
    ll ans=0;
    map<int,int> mp;
    for(int i=1;i<=n;i++){
        mp[s[i-1]]++;
        if(i-k>=0)mp[s[i-k]]--;
        cin>>s[i];
        s[i]=(s[i-1]+s[i]-1)%k;
        ans+=mp[s[i]];
    }
    cout<<ans<<endl;
    return 0;
}

L.和差 [ABC147F] Sum Difference

题面:

在这里插入图片描述

思路:

很难,但是收获颇多。

这是一个 n n n 项,初值为 X X X,公差为 D D D 的等差数列。我们只考虑是正了八经的等差数列的情况 ,也就是 D > 0 D>0 D>0 的情况。排除掉以下几种特殊情况:

  1. X = 0 , D = 0 X=0,D=0 X=0,D=0,这时只有 n n n 0 0 0,答案就是 1 1 1
  2. X ≠ 0 , D = 0 X\not=0,D=0 X=0,D=0,这时有 n n n X X X,答案就是 n + 1 n+1 n+1
  3. D < 0 D<0 D<0 ,我们把这个等差数列反过来,把终项 a n = X + ( n − 1 ) ∗ D a_n=X+(n-1)*D an=X+(n1)D 看作首项,这样公差就是 − D > 0 -D>0 D>0 了。

假设整个等差数列的和是 s u m sum sum,邱宇的选取的数的总和为 w w w,那么妹妹选取的数的总和为 s u m − w sum-w sumw ,两人数的差值就是 2 ∗ w − s u m 2*w-sum 2wsum发现其实求两人手上数的差值的种数其实就是求邱宇选取的数的总和 w w w 的种数

假如邱宇选取了的等差数列其中的 k k k 个数 ,那么 w = k ∗ X + t ∗ D w=k*X+t*D w=kX+tD t t t 最小当然是选取等差数列最小的 k k k 个, D D D 的系数也就是 0 + 1 + ⋯ + k − 1 = k ∗ ( k − 1 ) 2 0+1+\dots+k-1=\dfrac{k*(k-1)}2 0+1++k1=2k(k1),最大是选取等差数列最大的 k k k 个, D D D 的系数也就是 ( n − k + 1 ) + ⋯ + n = ( 2 ∗ n − k + 1 ) ∗ k 2 (n-k+1)+\dots+n=\dfrac{(2*n-k+1)*k}2 (nk+1)++n=2(2nk+1)k。那么 t t t 能取到中间的值吗,答案是可以。我们从最小的选取情况,把最后一个选取的数换成后一个数,这样 t t t + 1 +1 +1 了,知道换到最后一个,然后换倒数第二个,以此类推,可以把所有数换到最后 k k k 个,这样就取到了中间所有数。

现在知道了 w = k ∗ X + t ∗ D w=k*X+t*D w=kX+tD,那么怎么求得 w w w 的种数呢,总不能一个一个标记吧。神奇的来了, k k k 确定时, w w w 这个数的所有取值这时相当于模 D D D 同余的 , k k k 不同时, w w w D D D 同余的余数是看 k ∗ X k*X kX 的。我们把数轴上 w w w D D D 同余的位置都取出来, w w w 的取值相当于覆盖了上面的一段区间,我们给这些位置都除以 D D D,它们就贴在一起了,互相之间只差 1 1 1,就变成了真正的一段区间,这段区间也就是: [ ⌊ k ∗ X D ⌋ + k ∗ ( k − 1 ) 2 , ⌊ k ∗ X D ⌋ + ( 2 ∗ n − k + 1 ) ∗ k 2 ] \left[\left\lfloor\dfrac{k*X}{D}\right\rfloor+\dfrac{k*(k-1)}2,\left\lfloor\dfrac{k*X}{D}\right\rfloor+\dfrac{(2*n-k+1)*k}2\right] [DkX+2k(k1),DkX+2(2nk+1)k]我们对每个 k k k 都计算一下它的余数,余数相同的区间之间区间求并,余数不同的区间分开计算答案,最后把答案加起来就可以了。

code:

#include <iostream>
#include <cstdio>
#include <vector>
#include <set>
#include <map>
#define mk make_pair
using namespace std;
typedef long long ll;
const ll inf=1e18;

ll x,d,n;
map<ll,set<pair<ll,ll> > > mp;

int main(){
	cin>>n>>x>>d;
	if(d==0 && x==0)return cout<<1,0;
	if(d==0 && x!=0)return cout<<n+1,0;
	if(d<0){
		x=x+(n-1)*d;
		d=-d;
	}
	for(ll k=0,a,l,r;k<=n;k++){
		a=k*x;
		l=k*(k-1)/2+a/d;
		r=(n-k+n-1)*k/2+a/d;
		mp[(a%d+d)%d].insert(mk(l,r));
	}
	ll ans=0;
	for(auto S:mp){
		ll l=-inf,r=-inf-1;
		for(auto x:S.second){
			if(x.first<=r)r=max(r,x.second);
			else {
				ans+=r-l+1;
				l=x.first;
				r=x.second;
			}
		}
		ans+=r-l+1;
	}
	cout<<ans;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值