NEFU大一暑假集训-同余

题集链接

OP

感谢学长的讲解与付出;
感谢ph和zsl两位大佬的指导与讨论;

夹点私货:
这些题涉及到的内容都已经更新到自己的笔记中:
数学(快速幂,欧拉函数,欧拉定理,扩展欧里几得算法,逆元)
树与图论(Prufer数列)
排列组合(lucas定理)

A Relatives

题目大意

对于给定n,输出小于n且与n互质的数的个数

思路

第一反应即是欧拉函数值,不过需要注意的是 φ ( 1 ) = 1 \varphi(1)=1 φ(1)=1 ,而按此题要求,对于1应输出0;

代码
#include <cstring>
#include <ctime>
#include <iostream>
#include <math.h>
using namespace std;
typedef long long ll;

const ll N=1e7;
int p[N+7],c=0;//p[]存储第 i 个质数(from 0),c 作为末尾标记
bool n[N+7];//n[]存储整数 i 是否为质数,若标记 0 则为质数,标记 1 则为合数
void pri()
{
	n[0]=n[1]=1;//0,1 均不是质数
	for(int i=2;i<=N;i++)
	{
		if(!n[i])p[c++]=i;//若为质数,则加入 p 数组
		for(int j=0;j<c&&i*p[j]<=N;j++)
		{
			n[i*p[j]]=1;
			if(i%p[j]==0)break;
		}
	}
}

int main() {
    pri();
    ll g,ans;
    while(scanf("%lld",&g)&&g)
    {
        ans=g;
        if(g==1){printf("0\n");continue;}
        int sq=sqrt(g);
        for(int i=0;i<c&&p[i]<=sq;i++)
        {
            if(g%p[i]==0)
            {
                ans=ans-ans/p[i];
                while(g%p[i]==0)
                {
                    g/=p[i];
                }
            }
        }
        if(g>1)
        ans=ans-ans/g;
        printf("%lld\n",ans);
    }
    return 0;
}

B Brute-force Algorithm

题目大意

对于给定 a,b,P,n 输出func函数被执行的次数对P取模的模数;

思路

手动模拟发现,不考虑取模的情况下:
n = 1 , a n s = a ; n = 2 , a n s = b ; n = 3 , a n s = a b ; n = 4 , a n s = a b 2 ; n = 5 , a n s = a 2 b 3 ; n = 6 , a n s = a 3 b 5 ; n=1,ans=a;\\ n=2,ans=b;\\ n=3,ans=ab;\\ n=4,ans=ab^2;\\ n=5,ans=a^2b^3;\\ n=6,ans=a^3b^5; n=1,ans=a;n=2,ans=b;n=3,ans=ab;n=4,ans=ab2;n=5,ans=a2b3;n=6,ans=a3b5;
假设 n = i n=i n=i时,a和b的幂次分别为 A i , B i A_i,B_i Ai,Bi,我们可以发现有 A i = A i − 1 + A i − 2 , B i = B i − 1 + B i − 2 A_i=A_{i-1}+A_{i-2},B_i=B_{i-1}+B_{i-2} Ai=Ai1+Ai2,Bi=Bi1+Bi2 ,同时有 A 1 = 1 , B 1 = 0 , A 2 = 0 , B 2 = 1 A_1=1,B_1=0,A_2=0,B_2=1 A1=1,B1=0,A2=0,B2=1

规律很像斐波那契数列,我们就可以用矩阵快速幂进行快速计算了;

注意对矩阵元素取模的过程实际上是降幂过程,而不是简单的取模过程,所以要依照降幂的基本法;

代码
#include <algorithm>
#include <iostream>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

ll x, y, gn, m, phim;

ll qm(ll a, ll b)
{
    ll ans = 1 % m;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = ans * a % m;
        a = a * a % m;
    }
    return ans;
}

struct mtx
{
    ll a[2][2];
};

mtx mul(mtx A, mtx B)
{
    mtx C;
    memset(C.a, 0, sizeof(C.a));
    for (int i = 0; i < 2; i++)
        for (int j = 0; j < 2; j++)
            for (int k = 0; k < 2; k++)
            {
                C.a[i][j] += A.a[i][k] * B.a[k][j];
                if(C.a[i][j]>phim)C.a[i][j] %= phim,C.a[i][j] += phim;
            }
    return C;
}

mtx mqm(mtx A, ll b)
{
    mtx ans;
    memset(ans.a, 0, sizeof(ans.a));
    for (int i = 0; i < 2; i++)
        ans.a[i][i] = 1;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = mul(ans, A);
        A = mul(A, A);
    }
    return ans;
}

const ll N = 1000;
int p[N + 7], c = 0; //p[]存储第 i 个质数(from 0),c 作为末尾标记
bool n[N + 7];       //n[]存储整数 i 是否为质数,若标记 0 则为质数,标记 1 则为合数
void pri()
{
    n[0] = n[1] = 1; //0,1 均不是质数
    for (int i = 2; i <= N; i++)
    {
        if (!n[i])
            p[c++] = i; //若为质数,则加入 p 数组
        for (int j = 0; j < c && i * p[j] <= N; j++)
        {
            n[i * p[j]] = 1;
            if (i % p[j] == 0)
                break;
        }
    }
}

ll gphi(ll g)
{
    ll ans = g;
    if (g == 1)
        return 0;
    int sq = sqrt(g);
    for (int i = 0; i < c && p[i] <= sq; i++)
    {
        if (g % p[i] == 0)
        {
            ans = ans - ans / p[i];
            while (g % p[i] == 0)
            {
                g /= p[i];
            }
        }
    }
    if (g > 1)
        ans = ans - ans / g;
    return ans;
}

int main()
{
    pri();
    int t;
    cin >> t;
    for (int i = 1; i <= t; i++)
    {
        scanf("%lld%lld%lld%lld", &x, &y, &m, &gn);
        if (m == 1)
        {
            printf("Case #%d: %lld\n", i, 0);
            continue;
        }
        else if (gn == 1)
        {
            printf("Case #%d: %lld\n", i, x%m);
            continue;
        }
        phim = gphi(m);
        mtx k = {{{0, 1}, {1, 1}}};
        k = mqm(k, (gn - 2));
        //printf("%lld ",phim);
        //printf("%lld %lld ",k.a[1][0],k.a[1][1]);
        ll ans = qm(x, k.a[1][0]) * qm(y, k.a[1][1]) % m;
        //
        printf("Case #%d: %lld\n", i, ans);
    }
}

C Sum

题目大意

S ( k ) S(k) S(k) 为将N拆分为k份的方案数,对于给定N,求 ∑ i = 1 n S ( i ) \sum_{i=1}^nS(i) i=1nS(i) 对1e9+7取模的值;

思路

由插板法可知 S ( k ) = C n − 1 k − 1 S(k)=C_{n-1}^{k-1} S(k)=Cn1k1 ,故 ∑ i = 1 n S ( i ) = 2 n − 1 \sum_{i=1}^nS(i)=2^{n-1} i=1nS(i)=2n1
接下来对2进行降幂即可;

代码
#include <algorithm>
#include <iostream>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

const ll m=1e9+7;

ll qm(ll a, ll b)
{
    ll ans = 1 % m;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = ans * a % m;
        a = a * a % m;
    }
    return ans;
}

int main()
{
    string g;
    ll mi=0;
    while(cin>>g)
    {
        mi=0;
        for(int i=0;g[i];i++)
        {
            mi*=10;
            mi+=g[i]-'0';
            mi%=m-1;
        }
        mi-=1;
        if(mi<0)mi+=m-1;
        //printf("%lld ",mi);
        printf("%lld\n",qm(2,mi));
    }
}

D 同余方程

思路

实际上即是求a对b的逆元,题目保证有解即逆元存在;

由于未保证b为质数,qm(a,b-2)的方法便无法使用,我们可以用扩展欧几里得算法求解;

代码
#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

const ll m=1e9+7;

ll qm(ll a, ll b)
{
    ll ans = 1 % m;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = ans * a % m;
        a = a * a % m;
    }
    return ans;
}

ll exgcd(ll a,ll b,ll &x,ll &y)
{
   if(!b)
   {
      x=1,y=0;
      return a;
   }
   ll ans=exgcd(b,a%b,x,y);
   ll y1=y,x1=x;
   x=y1;
   y=x1-a/b*y1;
   return ans;
}

int main()
{
    ll a,b;
    cin>>a>>b;
    ll x,y;
    exgcd(a,b,x,y);
    printf("%lld",(x%b+b)%b);//防负
}

E SHUFFLE 洗牌

思路

简单模拟可以发现,洗牌是会循环进行的,即顺序牌洗若干次之后便会回到顺序,首先的处理我们可以将洗牌次数对循环长度取模;

分析易知对于原来第 i 位的牌,洗过一次后即为第 2*i+((i<=n>>1)?0:-(n+1))位;

如果洗牌次数对循环长度(m)取模后还剩n次,即可认为洗完n次后第L位的牌,再洗m-n次即可回到本身位置;

由此我们可以找出洗n次后第L位的牌是几号牌了;

代码
#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

int main()
{
    ll n,m,l;
    cin>>n>>m>>l;
    int ccl=0;
    ll i=1;
    do
    {
        ccl++;
        i=2*i+((i<=n>>1)?0:-(n+1));
    } while (i!=1);
    m%=ccl;
    m=ccl-m;
    i=l;
    for(int j=1;j<=m;j++)
    {
        i=2*i+((i<=n>>1)?0:-(n+1));
    }
    cout<<i;
}

F 树的计数

思路

此题是Prufer数列的一个结论
在这里插入图片描述
在实际解决中,还需要特判不成树的数据;

代码

(我认为的)正解代码:
预处理组合数的值,由于总答案小于1e17,所以每个组合数不会超过1e17,而且组合数的范围一定小于 C 150 150 C_{150}^{150} C150150

#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

//const ll M=100000000000000013;
ll c[152][152];
int n;
 void pre(){
	for(int i=0;i<=n;i++){
		c[i][0]=1;
		for(int j=1;j<=i;j++)
			c[i][j]=c[i-1][j]+c[i-1][j-1];
	}
}
ll lefts[152],d[152];
int main()
{
    cin>>n;
    pre();
    ll sum=0,ans=1;
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&d[i]);
        sum+=d[i];
        if((!d[i]&&n!=1)||d[i]<0){printf("0");return 0;}
        else if(!d[i]&&n==1){printf("1");return 0;}
        lefts[i]=lefts[i-1]+d[i]-1;
        ans*=c[n-2-lefts[i-1]][d[i]-1];
    }
    if(sum==2*n-2)printf("%lld",ans);
    else printf("0");
    return 0;
}

在vj上AC,但是在洛谷上被一组样例卡的代码:(目前不知道原因)
如果保证答案小于1e17,是不是可以等价于答案可以对1e17+3(大于1e17的最小质数)取模?

#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

const ll M=100000000000000003;

ll qm(ll a, ll b)
{
    ll ans = 1 % M;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = ans * a % M;
        a = a * a % M;
    }
    return ans;
} 
ll comp(ll a,ll b,ll m){
    if(a<b) return 0;
    if(a==b) return 1;
    if(b>a-b) b=a-b;
    ll ans=1,ca=1,cb=1;
    for(int i=0;i<b;i++){
        ca=ca*(a-i)%m;
        cb=cb*(b-i)%m;
    }
    ans=ca*qm(cb,m-2)%m;
    return ans;
}
/*
ll lucas(ll a,ll b,ll m){
    ll ans=1;
    while(a&&b){
        ans=(ans*comp(a%m,b%m,m))%m;
        a/=m;
        b/=m;
    }
    return ans;
}
*/
ll lefts[152],d[152];
int main()
{
    int n;
    cin>>n;
    ll sum=0,ans=1;
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&d[i]);
        sum+=d[i];
        if(!d[i]&&n!=1){printf("0");return 0;}
        lefts[i]=lefts[i-1]+d[i]-1;
        ans*=comp(n-2-lefts[i-1],d[i]-1,M);
        ans%=M;
    }
    if(sum==2*n-2)printf("%lld",ans);
    else printf("0");
}

G AquaMoon and Chess

题目大意

给定格子总数和占用情况,棋子可以以类似于跳棋的规则移动,求含初始状态一共可能存在多少种可能情况(对 998 , 244 , 353 998,244,353 998,244,353 求余);

思路

简单模拟后发现此问题与组合数有关,具体规律并不好描述,详见代码;

(目前还没证明)答案可以用以下规律抽象表示:
假设每个独立的占用区间长度为 l i l_i li,格子中共有 z z z个0,(满足格子总数= z + ∑ i = 1 n l i z+\sum_{i=1}^{n}l_i z+i=1nli),答案为 C z + ∑ i = 1 n ⌊ l i 2 ⌋ z C_{z+\sum_{i=1}^{n}\lfloor \frac {l_i} 2\rfloor}^z Cz+i=1n2liz

代码
#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <set>
#include <math.h>
#include <string.h>
using namespace std;
using ll = long long;

const ll M=998244353;

ll qm(ll a, ll b)
{
    ll ans = 1 % M;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = ans * a % M;
        a = a * a % M;
    }
    return ans;
} 
ll comp(ll a,ll b,ll m){
    if(a<b) return 0;
    if(a==b) return 1;
    if(b>a-b) b=a-b;
    ll ans=1,ca=1,cb=1;
    for(int i=0;i<b;i++){
        ca=ca*(a-i)%m;
        cb=cb*(b-i)%m;
    }
    ans=ca*qm(cb,m-2)%m;
    return ans;
}

ll lucas(ll a,ll b,ll m){
    ll ans=1;
    while(a&&b){
        ans=(ans*comp(a%m,b%m,m))%m;
        a/=m;
        b/=m;
    }
    return ans;
}

int main()
{
    int t,lo;
    cin>>t;
    string g;
    while(t--)
    {
        scanf("%d",&lo);
        cin>>g;
        ll n=0,m=0,l=-1,i;
        for(i=0;i<lo;i++)
        {
            if(g[i]=='0')
            {
                m++,n++;
                n+=(i-l-1)>>1;
                l=i;
            }
        }
        n+=(i-l-1)>>1;
        printf("%lld\n",lucas(n,m,M));
    }
}

ED

做这个题中大量用到自己以前准备的板子,同时也发现了网上扒下来的板子存在的很多问题和错误,大家注意辨别~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值