Skip to content

构造

递推构造+矩阵加速

矩阵乘法有介绍。

P3990

  1. 朴素\(dp\)
\[ f[i][j]= \left\{ \begin{array}{ll} f[i+1][j-1]+f[i+1][j-3]+...\\f[i][j-1]+f[i][j-3]+...\\f[i-1][j-1]+f[i-1][j-3]+... \end{array} \right . \]

复杂度\(O(nm^2)\)

2.小优化\(dp\)

注意到 $$ f[i][j-2]= \left{ \begin{array}{ll} f[i+1][j-3]+...\f[i][j-3]+...\f[i-1][j-3]+... \end{array} \right . $$

因此

\[ f[i][j]=f[i-1][j-1]+f[i][j-1]+f[i+1][j-1]+f[i][j-2]\times [j\geq 4] \]

注意特判,因为只有\(j\geq 4\)时才有\(f[i][j-3]...\)这些项,才需要加上\(f[i][j-2]\),不然会重复。如\(f[1][3]\)只会加上\(f[1][1]\)这个初始值,而\(f[1][1]\)不是递推得来的。

复杂度\(O(nm)\)

  1. 矩阵加速
\[\left[ \begin{matrix} f_{1,j}\\f_{2,j}\\...\\f_{n,j}\\f_{1,j-1}\\f_{2,j-1}\\...\\f_{n,j-1} \end{matrix} \right]= S \times \left[ \begin{matrix} f_{1,j-1}\\f_{2,j-1}\\...\\f_{n,j-1}\\f_{1,j-2}\\f_{2,j-2}\\...\\f_{n,j-2} \end{matrix} \right] \\ n=3时,S=\\ \left[ \begin{matrix}1&1&0&1&0&0\\1&1&1&0&1&0\\0&1&1&0&0&1\\1&0&0&0&0&0\\0&1&0&0&0&0\\0&0&1&0&0&0 \end{matrix}\right] \]
#include<iostream>
#include<cstdio>
#include<cstring>
#define int long long 
using namespace std;
const int N=110,P=30011;
int n,m;
int g[N][N];
struct ma{
    int f[N][N],n,m;
    ma(){
        memset(f,0,sizeof f),n=m=0;
    }
}s,t,ans;
ma operator *(ma a,ma b){
    ma c=ma();
    int x=a.n,y=a.m,z=b.m;
    c.n=x,c.m=z;
    for(int i=0;i<x;++i)
        for(int j=0;j<z;++j)
            for(int k=0;k<y;++k) c.f[i][j]=(c.f[i][j]+a.f[i][k]*b.f[k][j]%P)%P;
    return c;
}
ma kp(ma x,int p){
    if(p<=1) return x;
    if(p&1) return x*kp(x*x,p>>1);
    else return kp(x*x,p>>1);
}
void init(){
    t.n=t.m=n+n;
    for(int i=0;i<n;++i){
        int l=max(0ll,i-1),r=min(n-1,i+1);
        for(int j=l;j<=r;++j) t.f[i][j]=1;
        t.f[i][n+i]=t.f[n+i][i]=1;  
    }
    memset(g,0,sizeof g);
    g[1][1]=1;
    for(int j=2;j<=3;++j){
        for(int i=1;i<=n;++i){
            if(i+1<=n) g[i][j]+=g[i+1][j-1];
            if(i-1>=1) g[i][j]+=g[i-1][j-1];
            g[i][j]+=g[i][j-1];
            if(j-3>=1) g[i][j]+=g[i][j-2];
        }
    }
    s.n=n+n,s.m=1;
    for(int i=0;i<n;++i) s.f[i][0]=g[i+1][3];
    for(int i=n;i<n+n;++i) s.f[i][0]=g[i-n+1][2];
}
signed main(){
    scanf("%lld%lld",&n,&m);
    init();
    if(m<=3){
        printf("%lld",g[n][m]%P);       
    }else{
        ans=kp(t,m-3)*s;
        printf("%lld",ans.f[n-1][0]%P);
    }
    return 0;
}

方案构造

P7854

对于一个点想做别人的儿子,当且仅当它的权值\(a_i\)与父亲的权值\(a_j\)满足:\(a_j|a_i\).

所以,对于所有点,我们先按照权值大小从大到小排个序。

然后对于当前点\((u,a_u)\)将所有没有父亲节点并且\(a_u|a_v\)的点\(v\)连在\(u\)的下面。

同时,如果一个点有父亲节点\((p,a_p)\),\(a_v\)还是\(a_u\)的倍数,并且\(\gcd(a_p,a_u)=a_u\),说明\(a_p\)一定可以作为\(a_u\)的子节点(因为从大到小考虑,所以\(a_u\)一定比\(a_p\)小),那么跳过不用处理;

否则,说明\(a_p\)不能作为\(a_u\)的子节点,那么\(v\)同时有两个根节点\(u,p\),不满足,输出\(-1\).

建完树后,这棵树满足所有节点有唯一父亲节点并且权值都是父亲节点的倍数。

那么我们还需要判断是否出现\(\gcd(i,j)=k\)\(k\)没出现过的情况。

如:

3
1 4 6

这种。

首先,将所有出现过的,是\(k\)倍数的节点拿出来。如果这些节点在原树种组成了多颗子树,那么说明这些子树的根节点权值两两求\(\gcd\)一定能求出\(k\),而\(k\)不存在,则无解.

否则,如果只有一颗子树,就只会求出那个根节点,不会求出\(k\)

那么怎么判断呢?我们直接判断这些节点是否有父亲,或者其父亲节点是不是\(k\)的倍数时,如果没有父亲或者没有是\(k\)倍数的父亲,说明它是某颗子树的根节点,计数器加一;当计数器\(>1\)时,就无解了。

所以最终复杂度\(O((n+m)\log n)\)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1e5+10,M=1e6+10;
int gcd(int a,int b){
    return !b?a:gcd(b,a%b);
}
int n,cnt;
int fa[N],b[N],f[M],head[N];
vector<int> vis[M];
struct node{
    int id,val; 
}a[N];
bool cmp(node a,node b){
    return a.val>b.val;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i) scanf("%d",&a[i].val),a[i].id=i,b[i]=a[i].val,vis[a[i].val].push_back(a[i].id);
    sort(a+1,a+n+1,cmp);
    int lst=0,flag=1;
    for(int i=1;i<=n;++i){
        int pre=a[i].id;
        if(lst==a[i].val) continue;
        for(int j=a[i].val;j<M;j+=a[i].val){
            if(vis[j].size()){
                for(int k=0;k<vis[j].size();++k){
                    int now=vis[j][k];
                    if(now==pre) continue;
                    if(!fa[now]) fa[now]=pre;
                    else{
                        int g=gcd(b[fa[now]],b[pre]);
                        if(g!=b[pre]) flag=0;
                    }
                }
            }
        }
        lst=a[i].val;
    }
    for(int i=1;i<M;++i){
        int tmp=0;
        for(int j=i;j<M;j+=i){
            if(vis[j].size()){
                for(int k=0;k<vis[j].size();++k){
                    int now=vis[j][k];
                    if(fa[now]==0 || b[fa[now]]%i!=0) ++tmp;
                }
            }       
        }
        if(tmp>1){
            flag=0;break;
        }
    }
    if(!flag) printf("-1");
    else for(int i=1;i<=n;++i) printf("%d ",fa[i]);
    return 0;
}

P3557

注意到距离为1的情况的最优解就是距离为2的情况的一个可行解,因为距离为1时,两个塔之间最多只能间隔两个节点,这样才能保证塔数量最少且覆盖全图,而这种构造方式等价于距离为2的情况下将所有未覆盖的节点选作塔,并覆盖上周围点的构造方式。而题目保证一定可以构造出等于\(k\)的最优解\(1\)。因此这种构造方式一定能构造出\(\leq k\)的可行解\(2\)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=5e5+10,M=1e6+10;
int n,m,k,cnt,u,v;
int head[N],ans[N],vis[N];
struct edge{
    int v,nxt;
}e[M<<1];
void add(int u,int v){
    e[++cnt].v=v,e[cnt].nxt=head[u],head[u]=cnt;
}
void dfs(int u,int p,int dep){
    vis[u]=1;
    if(dep>=2) return;
    for(int i=head[u];~i;i=e[i].nxt){
        int v=e[i].v;
        if(v==p) continue;
        dfs(v,u,dep+1);
    }
}
int main(){
    memset(head,-1,sizeof head);cnt=-1;
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=m;++i) scanf("%d%d",&u,&v),add(u,v),add(v,u);
    for(int i=1;i<=n;++i) if(!vis[i]) ans[++ans[0]]=i,dfs(i,0,0);
    printf("%d\n",ans[0]);
    for(int i=1;i<=ans[0];++i) printf("%d ",ans[i]);
    return 0;
}

P5441

~~名副其实的MO题~~

初始答案显然,为\(C_n^4\)

定义\(S_k\)表示\(i\to ((i+k-1)\mod n+1)\)的所有边的集合,即跳过了\(k-1\)个点。\(S_k=S_{n-k},|S_k|=\frac n2\)

注意到最外层\(S_1\)/次外层\(S_2\)的边都可以按照一个方向放置,不需要双向边,对答案没有影响。

而对于\(k\geq 3\)的情况,由于前两层的边是按照一个方向放置的,所以会出现两个不同方向的环共用这一条边,即需要双向边。如果不用双向边,就会丢失一部分方案数。

那么我们贪心地取两环中最大环的方向作为这条边的方向,丢失一部分最小环上的方案数,这种边的数量为\(n\)条。(两环相等任取一个,但注意边数只有\(\frac n2\)条)

可以发现\(S_k\)的所有边可以组成所有点数为\(k\)的环。

小环方案数\(=C_{k-2}^2\),因为一个边的两个点一定要取,不然状态有重复。

那么我们按照从小到大的顺序,减去这些贡献,最后剩下的就是最大方案数。

至于方案,只需要在统计\(k\geq 3\)边的时候记录一下两个端点,将减去的设为单向,剩下的设为双向即可。再加上\(k\leq 2\)即可。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=110,M=1e5+10;
int C[N],c[N],f[N][N];
int n,cnt,sum,m;
struct node{
    int u,v,w;
    node(int uu=0,int vv=0,int ww=0){
        u=uu,v=vv,w=ww;
    }
}ans[M];
void work(){
    C[4]=1;c[2]=1;
    for(int i=5;i<=n;++i) C[i]=C[i-1]*i/(i-4);
    for(int i=3;i<=n;++i) c[i]=c[i-1]*i/(i-2);
    cnt=0;sum=C[n];
    for(int i=1;i<=n;++i)
        for(int j=1;j<=2;++j) f[i][(i+j-1)%n+1]=1,f[i][i]=0;

    for(int i=4;i<=n-i+2;++i){
        int tmp=c[i-2];
        if(i==n-i+2) for(int j=1;j<=n/2;++j) ans[++cnt]=node(j,(j+i-1-1)%n+1,tmp);//对应两环大小相等的情况。
        else for(int j=1;j<=n;++j) ans[++cnt]=node(j,(j+i-1-1)%n+1,tmp);    
    }
    m=max(0,n*(n-7)/2);
    for(int i=1;i<=m;++i){
        int u=ans[i].u,v=ans[i].v,w=ans[i].w;
        sum-=w;f[u][v]=1;
    }
    for(int i=m+1;i<=cnt;++i){
        int u=ans[i].u,v=ans[i].v;
        f[u][v]=f[v][u]=1;
    }   
}
int main(){
    scanf("%d",&n);
    work();
    printf("%d\n",sum);
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j) printf("%d ",f[i][j]);
        printf("\n");
    }
    return 0;
}