SCOI2018 D1T2 Numazu的蜜柑【二次剩余】

本文介绍了一道涉及树形DP和二次剩余的算法题目,通过Cipolla算法解决特定形式的方程,并使用DFS遍历树状结构来统计满足条件的节点对数量。
题目大意:

给定一棵有 n n 个节点的树,每个节点有点权ai给出 p,A,B p , A , B ,问有多少点对 (u,v) ( u , v ) 满足:
1. v v u的祖先。
2. a2u+Aauav+Ba2v0(modp) a u 2 + A a u a v + B a v 2 ≡ 0 ( mod p )

n100000,pP,3p1016,0A,B<p n ≤ 100000 , p ∈ P , 3 ≤ p ≤ 10 16 , 0 ≤ A , B < p

解题思路:

考场上不会二次剩余怎么解,还好暴力有55分……
回来再这里学习了一下解法,好像确实是板题。

用解一元二次方程的方法可得 auA±A24B2av(modp) a u ≡ − A ± A 2 − 4 B 2 a v ( mod p )
A24B A 2 − 4 B 为二次剩余,则用Cipolla算法直接计算 A24B A 2 − 4 B
否则答案统计0的对数。
算出 A24B A 2 − 4 B 后,直接dfs时用map统计即可。
时间复杂度为 O(nlogp) O ( n l o g p )

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll getint()
{
    ll i=0,f=1;char c;
    for(c=getchar();(c!='-')&&(c<'0'||c>'9');c=getchar());
    if(c=='-')c=getchar(),f=-1;
    for(;c>='0'&&c<='9';c=getchar())i=(i<<3)+(i<<1)+c-'0';
    return i*f;
}
const int N=100005;
int n,num,tot,first[N],nxt[N],to[N];
ll p,A,B,ans,a1,a2,w,det,a[N];
map<ll,int>cnt;
ll mul(ll x,ll y)
{
    ll res=0;
    for(;y;y>>=1,x=(x+x)%p)
        if(y&1)res=(res+x)%p;
    return res;
}
ll Pow(ll x,ll y)
{
    ll res=1;
    for(;y;y>>=1,x=mul(x,x))
        if(y&1)res=mul(res,x);
    return res;
}
struct Complex
{
    ll x,y;
    Complex(){}
    Complex(ll _x,ll _y):x(_x),y(_y){}
    inline friend Complex operator * (const Complex &a,const Complex &b)
    {return Complex((mul(a.x,b.x)+mul(mul(a.y,b.y),w))%p,(mul(a.x,b.y)+mul(a.y,b.x))%p);}
    inline friend Complex Pow(Complex &a,ll b)
    {
        Complex res=Complex(1,0);
        for(;b;b>>=1,a=a*a)
            if(b&1)res=res*a;
        return res;
    }
};
ll find(ll n)
{
    if(!n)return 0;
    ll a;
    while(1)
    {
        a=rand()%p;
        w=(mul(a,a)-n+p)%p;
        if(Pow(w,(p-1)/2)==p-1)break;
    }
    Complex res=Complex(a,1);
    res=Pow(res,(p+1)/2);
    return res.x;
}
void add(int x,int y)
{
    nxt[++tot]=first[x],first[x]=tot,to[tot]=y;
}
void dfs1(int u)
{
    ans+=cnt[a[u]];
    ll v1=mul(a1,a[u]),v2=mul(a2,a[u]);
    v1==v2?cnt[v1]++:(cnt[v1]++,cnt[v2]++);
    for(int e=first[u];e;e=nxt[e])
        dfs1(to[e]);
    v1==v2?cnt[v1]--:(cnt[v1]--,cnt[v2]--);
}
void dfs2(int u)
{
    if(a[u]==0)ans+=num,num++;
    for(int e=first[u];e;e=nxt[e])
        dfs2(to[e]);
    if(a[u]==0)num--;
}
int main()
{
    //freopen("lx.in","r",stdin);
    //freopen("lx.out","w",stdout);
    n=getint(),p=getint(),A=getint(),B=getint();
    for(int i=1;i<=n;i++)a[i]=getint();
    for(int i=2;i<=n;i++)add(getint(),i);
    det=(mul(A,A)-B*4%p+p)%p;
    if(Pow(det,(p-1)/2)!=p-1)
    {
        det=find(det);
        a1=mul((-A+det+p)%p,Pow(2,p-2));
        a2=mul((-A-det+p+p)%p,Pow(2,p-2));
        dfs1(1);
    }
    else dfs2(1);
    cout<<ans<<'\n';
    return 0;
}
### 关于 SCOI2009 WINDY 数的解法 #### 定义与问题描述 WINDY数是指对于任意两个相邻位置上的数字,它们之间的差至少为\(2\)。给定正整数区间\([L, R]\),计算该范围内有多少个WINDY数。 #### 动态规划方法解析 为了高效解决这个问题,可以采用动态规划的方法来处理。定义状态`dp[i][j]`表示长度为`i`且最高位是`j`的WINDY数的数量[^3]。 - **初始化** 对于单个数字的情况(即只有一位),显然每一位都可以单独构成一个合法的WINDY数,因此有: ```cpp dp[1][d] = 1; // d ∈ {0, 1,...,9} ``` - **状态转移方程** 当考虑多位数时,如果当前位选择了某个特定数值,则下一位的选择会受到限制——它必须满足与前一位相差不小于2的要求。具体来说就是当上一高位为`pre`时,当前位置可选范围取决于`pre`的具体取值: - 如果`pre >= 2`, 则可以选择`{0... pre-2}` - 否则只能从剩余的有效集合中选取 这样就可以通过遍历所有可能的状态来进行状态间的转换并累加结果。 - **边界条件处理** 特殊情况下需要注意的是,在实际应用过程中还需要考虑到给出区间的上下限约束。可以通过逐位比较的方式判断是否越界,并据此调整有效状态空间大小。 ```cpp // 计算不超过num的最大windy数数量 int calc(int num){ int f[15], g[15]; memset(f, 0, sizeof(f)); string s = to_string(num); n = s.size(); for (char c : s) { a[++len] = c - '0'; } // 初始化f数组 for (int i=0;i<=9;++i)f[1][i]=1; // DP过程省略... return sum; } long long solve(long long L,long long R){ return calc(R)-calc(L-1); } ``` 此代码片段展示了如何利用预处理好的`dp`表快速查询指定范围内的WINDY数总量。其中`solve()`函数用于返回闭区间\[L,R\]内符合条件的总数;而辅助函数`calc()`负责根据传入参数构建相应的状态序列并最终得出答案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值