DP1线性动态规划4题.

本文精选了四个典型算法问题,包括最长公共子序列、合唱队形安排、渡轮友好城镇连接及最长不下降子序列,通过具体实例展示了如何利用动态规划等方法解决实际问题。

例4 LCS(最长公共子序列)

一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X=<>,若另一序列Z=<>是X的子序列,是指存在一个严格递增的下标序列<>,使得对于所有j=1,2,…,k有

例如,序列Z=<B,C,D,B>是序列X=<A,B,C,D,A,B>的子序列,相应的递增下标序列为<2,3,5,7>

给定两个序列XY,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列XY的公共子序列。例如,若X=<A,B,C,B,D,A,B>Y=<B,D,C,A,B,A>,则序列<B,C,A>XY的一个公共子序列,序列<B,C,B,A>也是XY的一个公共子序列。而且,后者是XY的一个最长公共子序列,因为XY没有长度大于4的公共子序列。

最长公共子序列(LCS)问题:给定两个序列X=<>Y=<>,要求找出XY的一个最长公共子序列。

输入文件:

第一行为一个整数n(1≤n≤5000),表示X序列的长度;

第二行为长度为nX序列;

第三行为一个整数m(1≤m≤5000),表示Y序列的长度;

第四行为长度为mY序列。

输出文件:

XY序列的最长公共子序列的长度。

输入样例:

7

ABCBDAB

6

BDCABA

输出样例:

4

#4.
#include <bits/stdc++.h>
#define N 5010
using namespace std;

char a[N],b[N];

int s[N][N];
int main()
{
    int n,m;
    char c;

    scanf("%d",&n);
    getchar();
    scanf("%s",a);
    getchar();
    scanf("%d",&m);
    getchar();
    scanf("%s",b);

    for(int i=0;i<n;++i)
    {
        for(int j=0;j<m;++j)
        {
            if(i==0&&j==0) s[i][j]=1;
            else if(a[i]==b[j]&&i!=0&&j!=0) s[i][j]=s[i-1][j-1]+1;
            else s[i][j]=max((i-1)>=0?s[i-1][j]:0,(j-1)>0?s[i][j-1]:0);
        }
    }

    printf("%d\n",s[n-1][m-1]);
    return 0;
}

例3 合唱队形(NOIP2004)

N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2,…,K,他们的身高分别为,,…,,则他们的身高满足 (1≤i≤K)。

你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

输入文件:

输入文件的第一行是一个整数N(2≤N≤100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130≤Ti≤230)是第i位同学的身高(厘米)。

输出文件:

输出文件包括一行,这一行只包含一个整数,就是最少需要几位同学出列。

输入样例:

8

186 186 150 200 160 130 197 220

输出样例:

4

数据规模:

对于50%的数据,保证有N≤20;

对于全部的数据,保证有N≤100。

#3.
#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    int hei[110];
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        scanf("%d",&hei[i]);

    int maxx=0;
    for(int i=2;i<n;++i)
    {
        int f[110];
        int p[110];
        for(int j=1;j<=n;++j)
            f[j]=1;
        for(int j=1;j<=n;++j)
            p[j]=1;
        for(int j=2;j<=i;++j)
        {
            for(int k=1;k<j;++k)
            {
                if(hei[j]>hei[k]) f[j]=max(f[j],f[k]+1);
                else {f[j]=f[j-1];break;}
            }
        }


        for(int j=i+1;j<=n;++j)
        {
            for(int k=i;k<j;++k)
            {
                if(hei[j]<hei[k]) p[j]=max(p[j],p[k]+1);
                else {p[j]=p[j-1];break;}
            }
        }
        //cout<<f[i]<<" "<<p[n]<<endl;
        maxx=max(maxx,f[i]+p[n]-1);
        //cout<<n-maxx<<endl;
    }

    printf("%d\n",maxx);
    return 0;
}

例2 渡轮问题

有一个国家被一条河划分为南北两部分,在南岸和北岸总共有n个城镇,每一城镇在对岸都有唯一的友好城镇。任何两个城镇都没有相同的友好城镇。每一对友好城镇都希望有一条航线来往,于是他们向政府提出了申请。由于河终年有雾,政府决定不允许有任两条航线交叉(如果两条航线交叉,将有很大机会撞船)。

你的任务是编写一个程序来帮政府官员决定他们应拨款兴建哪些航线以使得没有出现交叉的航线数量最多。

输入文件:

南北城市从左到右编号为1 – n(n≤5000);

第一行为n,以下n行每行两个数,表示友好的两个城市。

输出文件:

4

1 2

2 3

3 4

4 1

输出样例:

3

#2.
#include <bits/stdc++.h>
#define N 5010
using namespace std;

int main()
{
    int n;
    int a[N],b[N];
    int p[N];

    scanf("%d",&n);
    for(int i=1;i<=n;++i)
    {
        scanf("%d %d",&a[i],&b[i]);
    }

    for(int i=1;i<n;++i)
        for(int j=i+1;j<=n;++j)
        {
            if(a[i]>a[j])
            {
                int aa,bb;
                aa=a[i];
                a[i]=a[j];
                a[j]=aa;
                bb=b[i];
                b[i]=b[j];
                b[j]=bb;
            }
        }

    for(int i=1;i<=n;++i)
        p[i]=1;

        for(int i=2;i<=n;++i)
    {
        for(int j=1;j<n;++j)
        {
            if(b[i]>=b[j]) p[i]=max(p[i],p[j]+1);
            else p[i]=p[i-1];
        }

    }

    printf("%d\n",p[n]);
    return 0;
}

例1 求最长不下降子序列。

由n个不相同的整数组成的数列,记为:a(1),a(2),…,a(n)且a(i)≠a(j)(i≠j),例如,3,18,7,14,10,12,23,41,16,24.若存在i1<

i2<i3<…<ie且有a(i1)<a(i2)<…<a(ie),则称其为长度为e的不下降子序列。如上例中3,18,23,24就是一个长度为4的不下降子序列,同时也有3,7,10,12,16,24长度为6的不下降子序列。程序要求,当原数列给出之后,求出最长的不下降子序列的数据个数。

输入文件:

第一行为N(1≤N≤5000),第二行为N个整数,之间用空格隔开。

输出文件:

最长的不下降子序列的数据个数。

输入样例:

10

3 18 7 14 10 12 23 41 16 24

输出样例:

6


#1.
#include <bits/stdc++.h>
#define N 5010
using namespace std;

int main()
{
    int s[N],f[N];
    int n;
    int maxx=0;

    scanf("%d",&n);

    for(int i=1;i<=n;++i){
        scanf("%d",&s[i]);
        f[i]=1;
    }

    for(int i=2;i<=n;++i)
    {
        for(int j=1;j<n;++j)
        {
            if(s[i]>=s[j]) f[i]=max(f[i],f[j]+1);
            else f[i]=f[i-1];
        }

    }

    printf("%d\n",f[n]);
    return 0;
}


<think>我们面对的问:P3207 [HNOI2010] 染色问 目描述:给定一个包含n个点的环,用m种颜色给每个点染色。要求相邻的点颜色不同,且使用了m种颜色中的k种(即恰好使用k种颜色)。求方案数模1e9+7。 注意:环状结构,且要求恰好使用k种颜色。 思路: 1. 环状问通常可以转化为链状问,但要注意首尾相连的限制。 2. 由于要求恰好使用k种颜色,我们可以考虑先忽略环状结构,变成链状,然后通过容斥或者DP来考虑环。 然而,目要求动态规划解法(线性DP),但环状结构通常需要两次DP(一次断开成链,一次强制连接)。另外,恰好使用k种颜色,我们可以先考虑至少使用k种颜色,然后通过容斥原理得到恰好使用k种颜色。但目要求线性DP,所以我们需要设计状态来记录使用的颜色数。 但是,环上的染色问,经典做法是:先考虑一个长度为n的链,相邻不同色,且首尾颜色不同(因为环要求首尾不同),然后再加上首尾相同的处理?但这里要求恰好k种颜色。 另一种思路:破环为链,然后固定第一个点的颜色,这样链的第一个点颜色固定,然后进行DP,最后再考虑首尾颜色不同。但是这样会忽略颜色种类的计数。 我们设计状态: dp[i][j] 表示前i个点,使用了j种颜色,且相邻颜色不同的方案数。但是这样没有考虑环的首尾相连。 所以,我们分两步: 1. 先解决链的问:n个点排成一行,用m种颜色染,相邻不同,且使用了恰好k种颜色的方案数。 2. 再解决环的问:将链的首尾连接,要求首尾颜色不同。 但是,环上染色问,经典做法是:固定第一个点的颜色,然后DP,最后再通过调整保证首尾不同。然而,由于我们还要记录颜色种类,所以状态需要记录第一个点的颜色和当前点的颜色以及使用颜色数,这样状态会很大。 参考引用[4]中提到了容斥原理,我们可以考虑使用容斥原理来去掉使用了少于k种颜色的情况。 但是,目要求线性DP,所以我们尝试设计线性DP。 经典问:环上染色(相邻不同,不要求使用颜色数)的公式: (m-1)^n + (-1)^n*(m-1) [通过递推得到] 但是这里要求恰好k种颜色,所以我们需要先计算在环上使用恰好k种颜色的方案数。 步骤: 1. 首先,从m种颜色中选出k种颜色,有C(m,k)种选法。 2. 然后,用这k种颜色染一个环,相邻不同,且必须使用全部k种颜色(即恰好k种颜色,且相邻不同)。 但是,第二步中,环上使用k种颜色且相邻不同,但并没有要求必须使用全部k种颜色?不,我们要求的是恰好k种颜色,所以必须全部使用。 所以,问转化为:固定k种颜色,求环上染色,相邻不同,且每种颜色至少使用一次的方案数。 这是一个带环的“相邻不同,颜色全部使用”的问,类似于图的染色问。 我们可以用容斥原理:设f(i)表示至多使用i种颜色的方案数(即环上相邻不同,使用颜色在i种内),那么由容斥原理,恰好k种颜色的方案数为: sum_{i=0}^{k} (-1)^{k-i} * C(k, i) * f(i) 但是,这里f(i)表示用至多i种颜色染环,相邻不同的方案数。而环上相邻不同的方案数,我们知道为: (i-1)^n + (-1)^n*(i-1) [公式] 但是,这个公式是对于整个环的,而且没有考虑必须使用全部颜色?不,这个公式只是相邻不同,并不要求每种颜色都出现。所以f(i) = (i-1)^n + (-1)^n*(i-1) 这个公式是错的,因为它只保证了相邻不同,并没有要求每个点都染色,而且也没有要求颜色使用不超过i种?实际上,这个公式是使用i种颜色染环,相邻不同的方案数,但并没有要求每种颜色都出现。 所以,我们需要的是:在环上使用至多i种颜色(不一定全部出现)且相邻不同的方案数,这个方案数就是: (i-1)^n + (-1)^n*(i-1) 吗?不对,这个公式实际上是固定了颜色数为i,但每个点都可以任意选择这i种颜色中的一种,只要相邻不同。所以它包含了使用1种、2种...i种颜色的情况。 但是,我们要求的是至多使用i种颜色的方案数,那么就是使用1~i种颜色的方案数之和。然而,我们并没有一个直接的公式。所以,我们换一个思路。 实际上,我们要求的是:在环上使用恰好k种颜色(每种颜色至少用一次)且相邻不同的方案数。这个数等于: (k-1)^n + (-1)^n*(k-1) 再减去没有使用全部k种颜色的方案?但是这样又需要容斥。 所以,我们直接使用容斥原理:设g(k)为环上使用恰好k种颜色(每种颜色至少一次)且相邻不同的方案数。 g(k) = sum_{i=0}^{k} (-1)^{k-i} * C(k, i) * f(i) 其中,f(i)表示用i种颜色染环,相邻不同,且不限制每种颜色是否出现的方案数。那么f(i) = (i-1)^n + (-1)^n*(i-1) [注意:这个公式是环上使用i种颜色(不一定全部出现)且相邻不同的方案数?实际上,这个公式是固定了颜色总数为i,但每个点独立选择颜色(相邻不同),所以颜色可以不用全。因此,f(i)就是使用不超过i种颜色的方案数吗?不是,因为这里我们固定了颜色总数为i,所以每个点只能从这i种颜色中选择,所以f(i)就是使用颜色属于这i种颜色,相邻不同的方案数。所以,f(i)就是至多使用i种颜色的方案数(因为可能没有用到全部i种)。 所以,我们得到: g(k) = sum_{i=0}^{k} (-1)^{k-i} * C(k, i) * [ (i-1)^n + (-1)^n*(i-1) ] 但是,这个公式中,当i=0或1时需要注意: i=0: 没有颜色,方案数为0(因为n>=1,所以不可能) i=1: 当n>=2时,环上相邻不同,所以不能用1种颜色,所以方案数为0(除非n=1,但目没有n=1的情况,我们按一般情况n>=3考虑?目没有明确,但环至少3个点?不,目没有,所以n>=1) 但是,我们观察公式:当i=1时,公式为 (1-1)^n + (-1)^n*(1-1) = 0 + 0 = 0,正确。 当i=2时,公式为 (2-1)^n + (-1)^n*(2-1) = 1 + (-1)^n,这个值在n为偶数时为0,奇数时为2?但是实际上,两种颜色染环,相邻不同,且n为偶数时,有两种方案(交替染色),n为奇数时,则无法完成(因为环上奇数个点,相邻不同要求交替,但首尾会相同),所以n为奇数时方案数为0,n为偶数时方案数为2。但是公式计算n为偶数时:1+1=2,n为奇数时:1-1=0,正确。 所以,这个公式f(i) = (i-1)^n + (-1)^n*(i-1) 是正确的。 因此,整个问的答案就是: ans = C(m,k) * g(k) = C(m,k) * [ sum_{i=0}^{k} (-1)^{k-i} * C(k, i) * ( (i-1)^n + (-1)^n*(i-1) ) ] 但是,我们需要注意,当i=0时,C(k,0)=1,但是(i-1)在i=0时为-1,所以(-1)^n*(i-1)=(-1)^n*(-1)=(-1)^{n+1},而(i-1)^n = (-1)^n,所以f(0)= (-1)^n + (-1)^{n+1} = 0,所以i=0可以忽略。 因此,i从1到k。 所以,最终答案: ans = C(m,k) * sum_{i=1}^{k} (-1)^{k-i} * C(k,i) * [ (i-1)^n + (-1)^n*(i-1) ] 但是,我们还需要注意,当k=1时,如果n>=2,那么环上染色不可能只用1种颜色(因为相邻不同),所以方案数为0,而公式中当i=1时,f(1)= (1-1)^n + (-1)^n*(1-1)=0,所以正确。 但是,这个公式是否包含了环上染色必须使用全部k种颜色?是的,因为我们通过容斥原理,g(k)就是恰好使用k种颜色(每种至少一次)的方案数。 所以,我们得到了一个组合数学的解法,但目要求动态规划线性DP)。然而,这个组合数学公式已经可以计算,但目要求动态规划,所以我们需要用动态规划来解决。 动态规划解法: 由于是环,我们可以先固定第一个点的颜色,然后断开成链。但是要求使用恰好k种颜色,所以我们需要记录颜色使用数。 状态设计: 设dp[i][j]表示前i个点,使用了j种颜色,且第i个点的颜色与第一个点的颜色是否相同(因为最后要连接成环,所以首尾不能相同,但我们固定第一个点,所以需要记录最后一个点与第一个点的关系)。 但是,这样状态需要记录第一个点的颜色?我们可以通过固定第一个点颜色为1,然后最后再乘上m(因为第一个点可以任意选择颜色,但这样会重复计数,因为颜色是相对的,我们固定第一个点颜色为1,然后其他点可以使用1~k种颜色,但最后需要乘上C(m,1)?不,这样很麻烦。 另一种思路:破环为链,然后我们强制第一个点染颜色1,然后最后再通过调整来考虑颜色1是否被使用以及使用颜色数。 但是,我们要求使用恰好k种颜色,所以状态需要记录颜色使用数。 状态设计: dp[i][j][c] 表示前i个点,使用了j种颜色,且第i个点的颜色为c(c=0表示与第一个点相同,c=1表示与第一个点不同)的方案数。但是这样状态中颜色c的具体值我们并不关心,我们关心的是最后一个点与第一个点是否相同,以及使用的颜色数。 但是,我们还需要知道当前点与前一个点的关系(相邻不同),所以我们需要记录上一个点的颜色?不,我们只需要记录上一个点的颜色与第一个点的关系,以及上一个点的颜色是否与当前点的颜色冲突?这样状态会变得复杂。 更直接的状态:设dp[i][j][x][y] 其中i表示位置,j表示使用的颜色数,x表示第一个点的颜色(离散化,我们固定第一个点颜色为1,那么其他点颜色不能为1?不,其他点可以为1,也可以不为1),这样状态太大。 参考经典环状染色DP: 通常,我们设两个状态: dp1[i] 表示链上前i个点,相邻不同,且第i个点与第一个点颜色相同的方案数。 dp2[i] 表示链上前i个点,相邻不同,且第i个点与第一个点颜色不同的方案数。 转移: 对于dp1[i]:要求第i个点与第一个点相同,那么第i-1个点必须与第一个点不同(因为相邻不同,且第i个点颜色等于第一个点,所以第i-1个点不能等于第一个点),所以: dp1[i] = dp2[i-1] (因为第i-1个点与第一个点不同,那么第i个点只能取第一个点的颜色,所以只有1种选择) 对于dp2[i]:第i个点与第一个点不同,那么第i-1个点可以是相同也可以是不相同,但是第i个点需要与第i-1个点不同,且不等于第一个点(因为不同): 如果第i-1个点与第一个点相同(即dp1[i-1]),那么第i个点不能等于第一个点,所以有(m-1)种选择(因为总共有m种颜色,去掉第一个点的颜色,但是注意第i个点不能等于第一个点,也不能等于第i-1个点(即第一个点),所以实际有m-1种选择?不对,第i-1个点就是第一个点的颜色,所以第i个点不能等于第i-1个点的颜色,即不能等于第一个点的颜色,所以只能选择其他m-1种颜色(因为总共有m种颜色,去掉第一个点的颜色,剩下m-1种),但是注意:第i个点不能等于第一个点的颜色,所以就是m-1种选择。 如果第i-1个点与第一个点不同(即dp2[i-1]),那么第i个点不能等于第i-1个点的颜色,也不能等于第一个点的颜色,所以有m-2种选择。 因此: dp2[i] = dp1[i-1]*(m-1) + dp2[i-1]*(m-2) 初始化: 第一个点:我们固定第一个点颜色为1。 那么,dp1[1]=1, dp2[1]=0 (因为第一个点就是第一个点,所以相同,不同为0) 然后,我们要求整个环,那么最后一个点(第n个点)不能与第一个点相同,所以方案数为dp2[n]。 但是,这里我们只考虑了相邻不同和首尾不同,并没有考虑使用颜色的种类数。所以我们需要在状态中加入颜色数。 因此,我们扩展状态: dp1[i][j]:前i个点,使用了j种颜色,且第i个点与第一个点颜色相同的方案数。 dp2[i][j]:前i个点,使用了j种颜色,且第i个点与第一个点颜色不同的方案数。 转移: 对于dp1[i][j]:第i个点颜色与第一个点相同,那么第i-1个点必须与第一个点不同(因为相邻不同),所以第i个点只能取第一个点的颜色(1种选择)。那么,第i个点使用第一个点的颜色,这个颜色在之前是否已经使用过?因为第一个点已经使用了这个颜色,所以这个颜色一定已经使用过,所以颜色数不会增加。因此: dp1[i][j] = dp2[i-1][j] (因为颜色数不变) 对于dp2[i][j]:第i个点与第一个点不同,那么第i-1个点可以是相同或不同。 情况1:第i-1个点与第一个点相同(即dp1[i-1][?]),那么第i-1个点的颜色就是第一个点的颜色,第i个点不能等于第i-1个点的颜色,也不能等于第一个点的颜色(因为不同),所以有m-1种选择(因为总共有m种颜色,去掉第一个点的颜色,还有m-1种)。但是,第i个点可以选择一种新颜色,也可以是已经使用过的旧颜色(非第一个点颜色)。 所以,这里需要分情况:第i个点选择的颜色是否为新颜色? 如果选择新颜色:那么需要从剩余的颜色中选择一种(剩余颜色数为:总颜色数m-当前已经使用的颜色数(不包括第i个点))。但是,当前已经使用的颜色数是多少?在dp1[i-1][j-1]中,已经使用了j-1种颜色(因为第i个点要使用一种新颜色,所以总颜色数变为j,那么第i-1个点的时候已经使用了j-1种颜色?不对,因为dp1[i-1]表示第i-1个点与第一个点相同,那么第一个点的颜色已经算在使用的颜色中,所以已经使用的颜色数就是j-1?不,dp1[i-1][j-1]表示使用了j-1种颜色,那么第i个点选择一种新颜色,那么总颜色数变为j,所以新颜色有(m-(j-1))种选择。 如果选择旧颜色:那么第i个点只能选择已经使用过的非第一个点颜色的颜色。已经使用过的颜色中,除了第一个点的颜色,还有(j-1)-1 = j-2种颜色(因为dp1[i-1][j-1]表示使用了j-1种颜色,其中包含第一个点的颜色,所以非第一个点颜色的有j-2种),所以有j-2种选择。 但是,注意:在dp1[i-1][j-1]中,第i-1个点颜色为第一个点颜色,那么已经使用过的颜色有j-1种(包括第一个点颜色),所以非第一个点颜色的有j-2种。所以,第i个点选择旧颜色(非第一个点颜色)有j-2种选择。 所以,对于dp1[i-1][j-1]转移来的部分:第i个点有 (m-1) 种选择,其中新颜色有 (m-(j-1)) 种,旧颜色有 (j-2) 种?不对,实际上,第i个点不能等于第一个点颜色,也不能等于第i-1个点颜色(即第一个点颜色),所以只能选择其他颜色。这些其他颜色分为两种:已经使用过的(非第一个点颜色)和未使用过的。所以: 新颜色:有 (m- (j-1)) 种 (因为已经使用了j-1种颜色,总共有m种,所以未使用的有m-(j-1)种,且这些颜色都不是第一个点颜色,因为第一个点颜色已经使用) 旧颜色:有 (j-1) - 1 = j-2 种 (因为已经使用的j-1种颜色中,去掉第一个点颜色,剩下的就是非第一个点颜色,有j-2种?不对,已经使用的j-1种颜色中,包含了第一个点颜色,所以非第一个点颜色有j-2种?不对,j-1种颜色包括第一个点颜色和另外j-2种颜色。所以非第一个点颜色的有j-2种。 所以,第i个点选择颜色的总方案数为:新颜色(j-1表示已经使用过的颜色数,包括第一个点颜色,所以未使用的颜色数为m-(j-1),但是这些未使用的颜色中,没有第一个点颜色(因为第一个点颜色已经使用),所以新颜色数就是m-(j-1)种;旧颜色:非第一个点颜色的旧颜色有(j-1)-1=j-2种?不对,因为已经使用过的颜色中,非第一个点颜色的有(j-1)-1 = j-2种。所以总方案数为: (m-(j-1)) + (j-2) = m-1 种,符合。 因此,对于dp1[i-1][j-1]转移来的部分,有 (m-1) 种选择,但是新颜色和旧颜色对颜色数的贡献不同: 如果选择新颜色,那么颜色数增加1,所以状态从dp1[i-1][j-1]转移到dp2[i][j](注意:这里j是第i个点之后的总颜色数,所以第i-1个点时的颜色数应该是j-1,因为第i个点选择新颜色,所以总颜色数从j-1变成j)。 如果选择旧颜色,那么颜色数不变,所以状态从dp1[i-1][j-1]转移到dp2[i][j-1]?不对,我们当前状态dp2[i][j]要求第i个点之后总颜色数为j,所以如果选择旧颜色,那么第i-1个点时的颜色数应该是j(因为第i个点没有增加新颜色),但是dp1[i-1][j-1]中第i-1个点时的颜色数只有j-1,所以不能直接转移。 所以,我们需要将选择新颜色和旧颜色分开讨论。 因此,转移方程需要拆开: dp2[i][j] += dp1[i-1][j-1] * (m - (j-1)) // 选择新颜色 dp2[i][j] += dp1[i-1][j] * (j-1 - 1) // 选择旧颜色,但是这里dp1[i-1][j]表示第i-1个点时已经使用了j种颜色,那么非第一个点颜色的旧颜色有j-1-1=j-2种?不对,因为dp1[i-1][j]中,已经使用了j种颜色,其中包含第一个点颜色,所以非第一个点颜色的有j-1种。那么第i个点选择旧颜色(非第一个点颜色)时,有j-1种选择?但是,第i个点不能等于第一个点颜色(已经满足,因为选择的是非第一个点颜色),而且不能等于第i-1个点颜色(即第一个点颜色),所以它可以选择任意一个非第一个点颜色,且不等于第i-1个点颜色(而第i-1个点颜色就是第一个点颜色,所以没有冲突),所以有j-1种选择?不对,因为非第一个点颜色有j-1种,而第i-1个点颜色是第一个点颜色,所以这些非第一个点颜色中,没有与第i-1个点颜色相同的(因为第i-1个点颜色是第一个点颜色,而这里的非第一个点颜色都不等于第一个点颜色),所以第i个点选择非第一个点颜色的任意一种都是可以的,所以有j-1种选择。 但是,这里我们要求第i个点与第i-1个点不同,而第i-1个点颜色是第一个点颜色,所以第i个点选择非第一个点颜色的任意一种都满足条件。所以,对于dp1[i-1][j]转移来的部分,有j-1种选择。 但是,注意:dp1[i-1][j]表示第i-1个点已经使用了j种颜色,那么非第一个点颜色有j-1种,所以第i个点选择旧颜色(非第一个点颜色)有j-1种选择。所以: dp2[i][j] += dp1[i-1][j] * (j-1) 但是,我们之前选择新颜色部分:从dp1[i-1][j-1]转移来,新颜色数有m-(j-1)种,所以: dp2[i][j] += dp1[i-1][j-1] * (m-(j-1)) 那么,对于dp1[i-1]的转移,我们拆成了两部分:一部分从j-1(颜色数)转移,一部分从j转移。 情况2:第i-1个点与第一个点不同(即dp2[i-1][?]),那么第i-1个点的颜色不是第一个点颜色,第i个点需要满足:不等于第i-1个点颜色,也不等于第一个点颜色。 所以,第i个点有m-2种选择?但是,同样,这里也要分新颜色和旧颜色(旧颜色指已经使用过的颜色,包括第一个点颜色和非第一个点颜色,但要去掉第i-1个点颜色和第一个点颜色)。 已经使用过的颜色:假设第i-1个点之前已经使用了j'种颜色。 第i个点选择: 新颜色:有 m - j' 种(因为总共有m种颜色,已经使用j'种) 旧颜色:有 j' - 2 种(因为不能等于第一个点颜色,也不能等于第i-1个点颜色,所以去掉两种) 所以,总方案数为: (m-j') + (j'-2) = m-2,符合。 因此,对于dp2[i-1][j-1](表示第i-1个点使用了j-1种颜色),那么选择新颜色:有m-(j-1)种,所以: dp2[i][j] += dp2[i-1][j-1] * (m-(j-1)) 对于dp2[i-1][j](表示第i-1个点使用了j种颜色),那么选择旧颜色:有j-2种(因为已经使用了j种颜色,去掉第一个点颜色和第i-1个点颜色,注意这两个颜色不同,因为第i-1个点颜色不是第一个点颜色),所以: dp2[i][j] += dp2[i-1][j] * (j-2) 所以,综合起来: dp1[i][j] = dp2[i-1][j] // 因为第i个点使用第一个点颜色,所以不会增加颜色数,所以第i-1个点必须已经使用了j种颜色(因为第i个点没有增加新颜色),且第i-1个点与第一个点不同(所以用dp2) dp2[i][j] = (dp1[i-1][j-1] * (m - (j-1))) + // 第i-1个点与第一个点相同,且第i个点选择新颜色 (dp1[i-1][j] * (j-1)) + // 第i-1个点与第一个点相同,且第i个点选择旧颜色(非第一个点颜色) -> 有j-1种选择(因为非第一个点颜色有j-1种?不对,这里我们重新考虑) 重新考虑:在dp1[i-1][j]中,第i-1个点与第一个点相同,那么第i-1个点的颜色就是第一个点颜色,而已经使用的颜色数为j(包含第一个点颜色)。那么第i个点要选择旧颜色(非新颜色),那么必须在已经使用的颜色中选,且不能等于第一个点颜色(因为第i个点与第一个点不同)且不能等于第i-1个点颜色(即第一个点颜色),所以不能选第一个点颜色。那么,在已经使用的颜色中,非第一个点颜色的有j-1种,所以有j-1种选择?不对,因为第i-1个点的颜色就是第一个点颜色,所以第i个点不能等于第i-1个点颜色,即不能等于第一个点颜色,所以只能选择非第一个点颜色,而这些颜色有j-1种(因为已经使用了j种颜色,去掉第一个点颜色,剩下j-1种)。所以,选择有j-1种。所以这一项是: dp1[i-1][j] * (j-1) (dp2[i-1][j-1] * (m - (j-1))) + // 第i-1个点与第一个点不同,且第i个点选择新颜色 (dp2[i-1][j] * (j-2)) // 第i-1个点与第一个点不同,且第i个点选择旧颜色:有j-2种选择(因为已经使用的j种颜色中,去掉第一个点颜色和第i-1个点颜色,有j-2种) 但是,注意:在dp1[i-1][j-1]中,第i-1个点与第一个点相同,且已经使用的颜色数为j-1,那么第i个点选择新颜色:有m-(j-1)种,然后颜色数从j-1增加到j,所以这一部分贡献给dp2[i][j]。 在dp1[i-1][j]中,第i-1个点与第一个点相同,且已经使用的颜色数为j,那么第i个点选择旧颜色(非第一个点颜色)有j-1种,而且颜色数不变(所以j不变),所以这一部分贡献给dp2[i][j](因为颜色数还是j)。 同理,dp2[i-1][j-1]贡献给dp2[i][j]:新颜色,颜色数增加。 dp2[i-1][j]贡献给dp2[i][j]:旧颜色,颜色数不变,选择有j-2种。 所以,转移方程: dp1[i][j] = dp2[i-1][j] // 只有一种选择,即选择第一个点的颜色,所以不会增加颜色数,所以第i-1个点必须已经有j种颜色,且与第一个点不同(所以用dp2) dp2[i][j] = dp1[i-1][j-1] * (m - j + 1) // 因为j-1,所以m-(j-1)=m-j+1 + dp1[i-1][j] * (j-1) + dp2[i-1][j-1] * (m - j + 1) + dp2[i-1][j] * (j-2) 注意:这里dp1[i-1][j-1]和dp1[i-1][j]是两种不同的状态,分别表示第i-1个点使用j-1种颜色和使用j种颜色。 但是,我们还需要考虑边界: i=1: 第一个点:dp1[1][1]=1 (因为第一个点与第一个点相同,使用了1种颜色) dp2[1][1] = 0 (第一个点不可能与第一个点不同) 但是,如果我们考虑使用颜色数,第一个点使用1种颜色,所以dp1[1][1]=1,其他dp1[1][j]=0 (j>1),dp2[1][j]=0 (任意j) 然后,我们要求的结果是:环上染色,使用恰好k种颜色,且相邻不同,且首尾不同(因为环)的方案数。 注意,我们这里DP的是链,但是我们已经固定第一个点,并且要求最后一个点不能与第一个点相同(因为环),所以方案数就是dp2[n][k]。 但是,这里我们固定第一个点颜色为1,而实际上第一个点可以选择任意颜色,所以最后答案要乘以m(因为第一个点有m种选择)?不对,因为我们DP中,第一个点固定为1,而其他点的颜色选择都是相对于这个1来进行的,所以如果我们不乘以m,那么方案数就是第一个点为1的情况,而第一个点可以是任意颜色,所以总方案数要乘以m。 但是,我们考虑:在DP过程中,第一个点颜色固定为1,那么其他点的颜色选择中,新颜色和旧颜色的计数都是基于这个1的。所以,整个方案数需要乘以C(m,1)=m,但是这样会重复计数吗?不会,因为第一个点选择不同颜色,方案是不同的。 然而,我们要求的是使用恰好k种颜色,那么第一个点选择一种颜色后,整个环使用的颜色数就是k,所以乘以m后,就是总方案数。 但是,我们注意:在DP中,我们使用的颜色数k是包括第一个点颜色的。所以,如果第一个点选择了一种颜色,那么整个环使用的颜色数就是k,所以没有问。 所以,最终方案数为:m * dp2[n][k] 但是,我们还需要考虑:环可以旋转,但是我们这里固定了第一个点,所以不会重复计数?因为环染色问中,旋转同构?目没有,通常计数问中,环上染色方案如果旋转后相同算同一种?但本给定了环上n个点,所以每个点位置固定,因此不是旋转同构,而是固定位置的环。所以,我们直接计数即可。 因此,我们设计动态规划dp1[1][1] = 1, 其他dp1[1][j]=0 dp2[1][j] = 0 (j>=1) for i from 2 to n: for j from 1 to min(i, k): // 使用的颜色数j不超过i,也不超过k dp1[i][j] = dp2[i-1][j] // 转移1 dp2[i][j] = (dp1[i-1][j-1] * (m - j + 1) if j>=1 else 0) + (dp1[i-1][j] * (j-1)) + (dp2[i-1][j-1] * (m - j + 1) if j>=1 else 0) + (dp2[i-1][j] * (j-2)) 注意:当j-2<0时,最后一项为0(即j>=2时,最后一项为dp2[i-1][j]*(j-2),否则0) 最后,答案 = m * dp2[n][k] (因为第一个点有m种选择,然后乘以dp2[n][k]) 但是,这个DP没有考虑环的首尾相连?我们这里dp2[n][k]表示第n个点与第一个点不同,所以已经保证了环的首尾不同。 然而,我们还需要注意:相邻不同在DP转移中已经保证了(因为每次转移都要求与上一个点不同)。 所以,这个DP就是可行的。 但是,时间复杂度:O(n*k),而目中n,k<=1000,m<=1e9,所以O(n*k)是1e6,可以接受。 然而,我们还需要注意:在DP中,第一维i可以滚动数组优化,但这里n,k最大1000,所以二维数组可以开(1000*1000=1e6)。 但是,我们还需要计算组合数?不,这里没有用到组合数,我们直接DP。 但是,我们还需要考虑:如果k>m,那么方案数为0,因为从m种颜色中选k种,不可能。 所以,先判断:如果k>m,则答案为0。 另外,如果k=0,那么n=0时方案数为1,否则为0?目要求使用k种颜色,所以k=0且n>0时,方案数为0。 但是,我们DP中,k>=1。 所以,边界条件要小心。 但是,目没有明n和k的范围,但根据目,n>=1, k>=1。 所以,我们写代码时,注意:当k=0时,如果n=0,则输出1,否则输出0。 但是,目要求的是环,所以n>=3?不,目没有,所以n>=1。 当n=1时,环上只有一个点,那么要求使用k种颜色,所以k=1,方案数为m(因为一个点染一种颜色,有m种选择),但是相邻没有限制(因为没有相邻点),所以满足。但是,我们DP中,n=1时,我们要求dp2[1][k](k=1)为0,然后乘以m=0,不对。 所以,我们需要单独处理n=1的情况。 当n=1时,答案:如果k=1,则方案数为m;如果k>1,则方案数为0(因为一个点只能染一种颜色)。 所以,我们可以在DP前特判n=1。 另外,当k=1且n>1时,方案数应该为0(因为相邻不同,所以一个环上至少两个点,不能只用一种颜色),而我们的DP中,当k=1且n>1时,dp2[n][1]应该为0。 所以,我们按照上述DP进行。 但是,我们还需要验证小数据。 例如:n=2, m=2, k=2。 两个点,两种颜色,要求使用两种颜色,相邻不同(环上相邻,所以两个点颜色不同,且使用两种颜色)。 方案数:第一个点有2种选择,第二个点有1种选择(只能选与第一个点不同的颜色),所以方案数为2*1=2。 我们DP: i=1: dp1[1][1]=1, dp2[1][1]=0. i=2: dp1[2][j] 和 dp2[2][j] 需要计算j=1,2。 j=1: dp1[2][1] = dp2[1][1] = 0. dp2[2][1] = dp1[1][0]*(m-0+1) -> 不存在(j-1=0,所以j>=1,但dp1[1][0]未定义,我们dp1[1][0]=0) + dp1[1][1]*(1-1) = 1*0 =0 + dp2[1][0]*(m-0+1) -> 0 + dp2[1][1]*(1-2) = 0 * (负数) =0 所以dp2[2][1]=0 j=2: dp1[2][2] = dp2[1][2] = 0 (因为dp2[1][2]未定义,我们设为0) dp2[2][2] = dp1[1][1]*(m-1+1) = 1 * (2-1+1)=2 // m=2, j-1=1 + dp1[1][2]*(2-1) = 0 *1 =0 + dp2[1][1]*(m-1+1)=0*(2)=0 + dp2[1][2]*(2-2)=0 = 2 所以,dp2[2][2]=2 然后,答案 = m * dp2[2][2] = 2 * 2 = 4,但正确答案是2。 错误原因:我们固定第一个点颜色为1,然后第二个点如果选择颜色2(新颜色),那么方案为1->2,然后乘以m(第一个点颜色选择)时,第一个点颜色选择有2种:1或2,所以: 第一个点选11->2 第一个点选2:2->1 所以总共2种,而我们的dp2[2][2]计算的是2,然后乘以m=2,得到4,显然重复了。 为什么重复?因为我们固定第一个点颜色为1,然后第二个点选择颜色2,这代表了一种方案(1->2)。而当我们乘以m(即第一个点可以选择任意颜色)时,如果我们选择颜色2,那么第二个点选择颜色2?不对,在DP中,第二个点选择颜色2是相对于第一个点颜色为1的情况,即颜色2是不同于1的另一种颜色。所以,当我们第一个点选择颜色2时,第二个点应该选择颜色1(因为不能和第一个点相同,且我们DP中第二个点选择新颜色,那么新颜色在DP中标记为2,但实际颜色编号是相对的,所以当我们第一个点选择2时,第二个点选择1,就对应了2->1。 但是,在DP过程中,我们并没有记录具体颜色,所以这种对应关系是成立的。所以,方案数应该是2,而我们得到4明我们多算了。 问在于:在DP中,我们第一个点固定为1,然后第二个点选择颜色2(新颜色),这代表了一种方案(1,2)。然后我们乘以m,相当于第一个点可以任意选择颜色,那么第一个点选择2时,第二个点选择颜色2(新颜色)?不对,在DP中,第二个点选择颜色2(新颜色)是相对于第一个点颜色为1的,所以当第一个点选择2时,第二个点应该选择什么?在DP中,我们并没有重新运行,所以第二个点选择颜色2(新颜色)在第一个点为2时,实际颜色编号为2,但如果我们把整个方案的颜色映射:1->2, 2->1,那么方案(第一个点2,第二个点1)实际上对应于(第一个1,第二个点2)的映射?所以,我们乘以m时,实际上是把所有方案都重复计数了,重复的次数等于颜色的排列数? 不对,我们只需要乘以第一个点选择的颜色数(m),但是这里我们DP中,第一个点固定为1,然后我们计算了第一个点为1的所有方案,然后乘以m,相当于每个方案我们都可以将第一个点颜色换成任意颜色,然后其他点颜色进行相应的替换?但是替换后,新颜色会变成什么? 实际上,我们DP中,我们只关心颜色是否相同,不关心具体值。所以,我们固定第一个点为1,然后其他点使用颜色:1,或者新颜色(用2,3,...表示)。然后,当我们给第一个点赋予任意颜色(比如c0)时,其他点中,标记为1的换成c0,标记为2的换成任意一种不同于c0的颜色,但是这样会乱。 所以,我们不应该乘以m。因为我们在DP中,第一个点固定为1,然后我们计算了第一个点为1的方案数,而第一个点选择其他颜色的方案数应该和1对称,所以总方案数就是 dp2[n][k] * C(m,1) = dp2[n][k]*m?但是,注意:在DP中,我们使用的颜色是抽象的,我们只关心第一个点的颜色,而其他新颜色都是相对于第一个点颜色的不同颜色。所以,当我们第一个点选择不同的颜色时,整个方案的染色方案是不同的,所以总方案数就是 dp2[n][k] * m。 但是,上面的例子中,我们得到dp2[2][2]=2,然后乘以m=2,得到4,但正确答案是2。为什么? 原因:在n=2,k=2时,我们DP中,第一个点固定为1,那么第二个点有两种选择:新颜色(颜色2)或者旧颜色(非第一个点颜色)?但是,在n=2时,第二个点不能与第一个点相同,所以只能选择新颜色(因为旧颜色只有第一个点颜色,不能选)。所以,在DP中,j=2时,第二个点选择新颜色,有m-1=1种选择(因为m=2,所以新颜色只有1种,即颜色2),所以方案数为1。但是我们计算dp2[2][2]时,用到了dp1[1][1]*(m-1+1)=1*2=2,所以多了。 我们重新看转移方程:对于dp2[2][2] = dp1[1][1] * (m-1+1) // 因为j=2,所以j-1=1,所以m-1+1 = m =2 + dp1[1][2]*(2-1) // 0 + dp2[1][1]*(m-1+1) //0 + dp2[1][2]*(2-2) //0 = 2 所以,dp2[2][2]=2,但实际方案只有1种(1->2)。 为什么是2?因为我们DP中,dp1[1][1]表示第一个点与第一个点相同,使用了1种颜色。那么第二个点,我们要求与第一个点不同,且使用2种颜色,所以必须选择新颜色。但是,新颜色有m-1=1种(因为m=2),所以方案数应该是1,而DP得到2。 检查:转移方程中,dp1[1][1] * (m-1+1) = 1 * (2) = 2,这里乘的是(m-1+1)=2,但实际新颜色只有1种。 错误原因:在dp1[1][1](即第一个点)时,已经使用的颜色数为1,那么第二个点选择新颜色时,新颜色数应该是m-1=1,而不是m-1+1=2。 我们再看转移方程:新颜色 = (m - (j-1)),其中j-1=1,所以m-1=1,所以应该是1,而不是2。 但是,在转移方程中,我们写的是 (m - j + 1),而j=2,所以m-2+1=2-1=1,所以应该是1。 所以,转移方程中,新颜色部分: (m - j + 1) [j是当前第i个点要使用的总颜色数] 在i=2,j=2时,m-j+1=2-2+1=1,所以 dp1[1][1]*(m-j+1)=1*1=1。 所以,我之前的代码中,写的是 (m - j + 1) 还是 (m - (j-1)) ? 在公式中,我们写的是 (m - j + 1) [因为m-(j-1)=m-j+1] 所以,上面的例子中,dp2[2][2] = 1 + 0 + 0 + 0 = 1,然后答案 = m * dp2[2][2] = 2 * 1 = 2,正确。 所以,转移方程中,新颜色部分使用 (m - j + 1) 是正确的。 因此,我们修正: dp2[i][j] = (j>=1 ? (dp1[i-1][j-1] * (m - j + 1)) : 0) + (j>=1 ? (dp2[i-1][j-1] * (m - j + 1)) : 0) + (dp1[i-1][j] * (j-1)) + (dp2[i-1][j] * (j-2)) 注意:最后一项,当j<2时,j-2<0,所以为0(即j>=2时才有)。 另外,dp1[i][j] = dp2[i-1][j] // 要求j>=1,因为dp2[i-1][j]中j>=1 所以,我们开始写代码,注意取模(模数=1e9+7)。 但是,空间:n*k=1000*1000=1e6,所以开二维数组,用两个二维数组:dp1[i][j]和dp2[i][j],i从1到n,j从1到k。 初始化: dp1[1][1] = 1 for j from 2 to k: dp1[1][j]=0 for j from 1 to k: dp2[1][j]=0 然后,从i=2到n,j=1到min(i,k)(注意j从1开始,并且j<=k): dp1[i][j] = dp2[i-1][j] [注意:这里dp2[i-1][j]可能为0] dp2[i][j] = 0 if j>=1: dp2[i][j] += dp1[i-1][j-1] * (m - j + 1) // 注意:乘法可能会爆int,用long long dp2[i][j] %= mod dp2[i][j] += dp2[i-1][j-1] * (m - j + 1) % mod dp2[i][j] %= mod dp2[i][j] += dp1[i-1][j] * (j-1) % mod dp2[i][j] %= mod if j>=2: dp2[i][j] += dp2[i-1][j] * (j-2) % mod dp2[i][j] %= mod 最后,答案 = (m * dp2[n][k]) % mod // 注意,这里m可能很大(1e9),所以乘法取模 但是,我们还要注意:如果n=1,则: if k==1: ans = m else: ans = 0 另外,如果k>m,则ans=0。 所以,我们写代码时,先判断n=1,然后判断k>m,然后进行DP。 但是,我们还需要注意:当k=0时,如果n=0,那么输出1,但目中n>=1,所以k=0时输出0。 所以,最终代码: if n==1: if k==1: ans = m else: ans = 0 else: if k>m: ans=0 else: // 初始化 dp1[1][1] = 1 // 其他为0 for i from 2 to n: for j from 1 to min(i, k): dp1[i][j] = dp2[i-1][j] // 这里dp2[i-1][j]是上一行的 // 然后计算dp2[i][j] = ... ans = (m * dp2[n][k]) % mod 但是,我们使用二维数组,注意内存,可以用滚动数组优化,但n=1000,所以开二维数组即可。 注意:取模,模数=1e9+7。 但是,我们还需要验证其他数据。 例如:n=3, m=2, k=2。 环上三个点,两种颜色,使用两种颜色,相邻不同。 方案:1->2->1,但是这样第三个点必须为1,则第二个点2和第三个点1不同,但第三个点1和第一个1相同,而环上相邻,所以第一个点和第三个点相邻,所以相同不行。 1->2->? 第三个点不能等于第二个点(2)也不能等于第一个点(1),所以没有颜色可选,所以方案数为0。 用组合数学公式:g(2)= sum_{i=1}^{2} (-1)^{2-i} * C(2,i) * [ (i-1)^3 + (-1)^3*(i-1) ] i=1: (-1)^1 * C(2,1) * [0 + (-1)*0] = 0 i=2: (-1)^0 * C(2,2) * [ (1)^3 + (-1)^3*(1) ] = 1 * (1 -1) =0 g(2)=0 ans = C(2,2)*0 =0 正确。 用DP: n=3, m=2, k=2. 初始化:dp1[1][1]=1, dp2[1][1]=0, 其他0。 i=2: j=1: dp1[2][1] = dp2[1][1] =0 dp2[2][1] = j-1=0,所以新颜色部分不计算(j>=1才计算新颜色,但j=1,所以j-1=0,所以新颜色部分:j-1=0,所以dp1[1][0]和dp2[1][0]都不存在,所以0;然后旧颜色部分:dp1[1][1]*(1-1)=1*0=0,dp2[1][1]*(1-2) 因为j=1<2,所以不计算。所以dp2[2][1]=0 j=2: dp1[2][2] = dp2[1][2]=0 (dp2[1][2]未定义,为0) dp2[2][2] = 新颜色:dp1[1][1]*(m-2+1)=1*(2-2+1)=1, dp2[1][1]*(m-2+1)=0 -> 1 旧颜色:dp1[1][2]*(2-1)=0, dp2[1][2]*(2-2)=0 -> 0 所以dp2[2][2]=1 i=3: j=1: 0 j=2: dp1[3][2] = dp2[2][2] = 1 dp2[3][2] = 新颜色:dp1[2][1]*(m-2+1) [dp1[2][1]=0,所以0] + dp2[2][1]*(m-2+1)=0 -> 0 旧颜色:dp1[2][2]*(2-1)= (dp1[2][2]=0) *1=0, dp2[2][2]*(2-2)=1*0=0 所以dp2[3][2]=0 所以,答案 = m * dp2[3][2] = 2*0=0,正确。 所以,这个DP可以。 但是,注意:在DP中,我们只计算j>=1且j<=k,所以j从1到min(i,k)。 最后,我们输出答案。 但是,目要求:P3207 挑战 染色 动态规划解法 线性DP 解 所以,我们按照上述思路写代码,但目要求是解,所以我们需要描述思路。 解描述: 目:给定环上n个点,用m种颜色染色,要求相邻不同,且使用恰好k种颜色,求方案数模1e9+7。 思路: 由于是环,我们固定第一个点的颜色,然后断开成链。我们设计两种状态: dp1[i][j]:表示前i个点,使用了j种颜色,且第i个点的颜色与第一个点相同的方案数。 dp2[i][j]:表示前i个点,使用了j种颜色,且第i个点的颜色与第一个点不同的方案数。 转移方程: dp1[i][j] = dp2[i-1][j] (因为第i个点要等于第一个点,那么第i-1个点必须与第一个点不同,且第i个点不会增加颜色数,所以使用颜色数j不变) dp2[i][j] = (如果j>=1) 从dp1[i-1][j-1]转移:此时第i-1个点与第一个点相同,第i个点选择新颜色,有(m-j+1)种选择。 (如果j>=1) 从dp2[i-1][j-1]转移:此时第i-1个点与第一个点不同,第i个点选择新颜色,有(m-j+1)种选择。 从dp1[i-1][j]转移:此时第i-1个点与第一个点相同,第i个点选择旧颜色(非第一个点颜色),有(j-1)种选择(因为已经使用的j种颜色中,非第一个点颜色有j-1种)。 (如果j>=2) 从dp2[i-1][j]转移:此时第i-1个点与第一个点不同,第i个点选择旧颜色(非第一个点颜色且非第i-1个点颜色),有(j-2)种选择。 注意:新颜色是指之前没有使用过的颜色,旧颜色是指已经使用过的颜色(非第一个点颜色)。 初始化: 第一个点:dp1[1][1]=1, 其他dp1[1][j]=0, dp2[1][j]=0 (所有j) 然后,我们要求的是环上染色,所以最后一个点(第n个点)不能与第一个点相同,因此方案数为 dp2[n][k]。 但是,我们固定第一个点颜色为1,而实际上第一个点可以任意选择颜色,所以总方案数 = m * dp2[n][k] (因为第一个点有m种选择)。 注意:当n=1时,需要特判:如果k=1,则方案数为m;否则为0。 当k>m时,方案数为0。 复杂度:O(n*k),可以通过。 最后,取模1e9+7。 所以,我们按照这个思路写代码。 但是,由于目要求动态规划解法,所以我们就用这个DP。 代码实现: 模数 = 1000000007 输入:n, m, k (目中,点数为n,颜色数m,要求使用k种颜色) if n==1: if k==1: print(m % mod) else: print(0) else: if k>m: print(0) else: # 初始化 dp1 = [[0]*(k+1) for _ in range(n+1)] dp2 = [[0]*(k+1) for _ in range(n+1)] # 第一行,i=1 dp1[1][1] = 1 # 使用1种颜色,且第一个点与第一个点相同(当然相同) # dp2[1][1] = 0 # 已经初始化为0 for i in range(2, n+1): for j in range(1, min(i+1, k+1)): # j从1到min(i,k) # 转移dp1 dp1[i][j] = dp2[i-1][j] # 直接等于,注意可能为0 # 转移dp2 # 新颜色部分 if j>=1: # 注意:j>=1,所以j-1>=0,但是数组下标j-1要>=0 # 从dp1[i-1][j-1]转移 dp2[i][j] = (dp2[i][j] + dp1[i-1][j-1] * (m - j + 1)) % mod # 从dp2[i-1][j-1]转移 dp2[i][j] = (dp2[i][j] + dp2[i-1][j-1] * (m - j + 1)) % mod # 旧颜色部分:从dp1[i-1][j]转移 dp2[i][j] = (dp2[i][j] + dp1[i-1][j] * (j-1)) % mod # 从dp2[i-1][j]转移,且j>=2 if j>=2: dp2[i][j] = (dp2[i][j] + dp2[i-1][j] * (j-2)) % mod ans = (m * dp2[n][k]) % mod print(ans) 但是,注意:在DP中,我们假设第一个点颜色固定,所以第一个点颜色为1,然后其他点选择颜色时,新颜色是相对于1的,所以第一个点选择任意颜色,方案数就是乘m。 然而,在DP过程中,我们使用的颜色数k是包括第一个点颜色的,所以正确。 我们测试一下n=2, m=2, k=2: 输出2,正确。 所以,我们输出解和代码。 注意:目要求中文回答。 所以,解如下: 目:P3207 染色 目描述:给定一个环,包含n个点,用m种颜色染色。要求相邻点颜色不同,且使用了恰好k种颜色。求方案数模1e9+7。 解法:动态规划线性DP) 思路: 由于是环,我们固定第一个点的颜色,然后断开成链。定义两个状态数组: - $dp1[i][j]$:表示前$i$个点,使用了$j$种颜色,且第$i$个点与第一个点颜色相同的方案数。 - $dp2[i][j]$:表示前$i$个点,使用了$j$种颜色,且第$i$$个点与第一个点颜色不同的方案数。 转移方程: - $dp1[i][j] = dp2[i-1][j]$ - $dp2[i][j] = $ - 若$j \\geq 1$,则加上$dp1[i-1][j-1] \\times (m - j + 1)$(表示第$i-1$个点与第一个点相同,第$i$个点选择新颜色) - 若$j \\geq 1$,则加上$dp2[i-1][j-1] \\times (m - j + 1)$(表示第$i-1$个点与第一个点不同,第$i$个点选择新颜色) - 加上$dp1[i-1][j] \\times (j-1)$(表示第$i-1$个点与第一个点相同,第$i$个点选择旧颜色) - 若$j \\geq 2$,则加上$dp2[i-1][j] \\times (j-2)$(表示第$i-1$个点与第一个点不同,第$i$个点选择旧颜色) 初始化:$dp1[1][1] = 1$,其余$dp1[1][j]=0$,$dp2[1][j]=0$。 最终,方案数为$m \\times dp2[n][k]$(因为第一个点有$m$种选择)。 特判: - 若$n=1$,则当$k=1$时,方案数为$m$;否则为0。 - 若$k>m$,则方案数为0。 复杂度:$O(nk)$,满足目要求。 代码实现(Python):</think>### P3207 染色问(环状动态规划解法) #### 问描述 给定一个环状结构($n$ 个点),使用 $m$ 种颜色染色。要求: 1. 相邻点颜色不同 2. 恰好使用 $k$ 种颜色 求方案数模 $10^9+7$。 #### 动态规划思路 由于环状结构首尾相连,我们采用**固定起点 + 链式DP**的策略: 1. **固定起点**:将环断开为链,固定第一个点的颜色 2. **状态定义**: - $dp1[i][j]$:前 $i$ 个点使用 $j$ 种颜色,且第 $i$ 个点与起点颜色相同的方案数 - $dp2[i][j]$:前 $i$ 个点使用 $j$ 种颜色,且第 $i$ 个点与起点颜色不同的方案数 3. **状态转移**: - $dp1[i][j] = dp2[i-1][j]$(第 $i$ 点必须与起点相同,故第 $i-1$ 点必须不同) - $dp2[i][j]$ 分四部分: - 新颜色($j \geq 1$):$dp1[i-1][j-1] \times (m - j + 1)$ - 新颜色($j \geq 1$):$dp2[i-1][j-1] \times (m - j + 1)$ - 旧颜色:$dp1[i-1][j] \times (j - 1)$ - 旧颜色($j \geq 2$):$dp2[i-1][j] \times (j - 2)$ 4. **初始化**: - $dp1[1][1] = 1$(起点自身相同) - 其余状态初始为 $0$ 5. **最终答案**: - 环要求首尾不同,故取 $dp2[n][k]$ - 起点有 $m$ 种选择,总方案数 $= m \times dp2[n][k]$ 6. **特判**: - $n=1$ 时:$k=1$ 则答案为 $m$,否则为 $0$ - $k > m$ 时:方案数为 $0$ #### 数学表达 $$ \begin{cases} dp1_{i,j} = dp2_{i-1,j} \\ dp2_{i,j} = [j \geq 1] \cdot (dp1_{i-1,j-1} + dp2_{i-1,j-1}) \cdot (m - j + 1) \\ \quad + dp1_{i-1,j} \cdot (j-1) \\ \quad + [j \geq 2] \cdot dp2_{i-1,j} \cdot (j-2) \\ \text{ans} = \begin{cases} m & n=1,\ k=1 \\ 0 & n=1,\ k \neq 1 \\ 0 & k > m \\ m \cdot dp2_{n,k} & \text{otherwise} \end{cases} \end{cases} $$ #### 代码实现(Python) ```python MOD = 10**9 + 7 def main(): n, m, k = map(int, input().split()) # 特判 n=1 if n == 1: print(m % MOD if k == 1 else 0) return # 特判 k>m if k > m: print(0) return # 初始化 DPdp1 = [[0] * (k + 1) for _ in range(n + 1)] dp2 = [[0] * (k + 1) for _ in range(n + 1)] dp1[1][1] = 1 # 起点固定颜色 # DP 转移 for i in range(2, n + 1): for j in range(1, min(i, k) + 1): # 转移 dp1 dp1[i][j] = dp2[i - 1][j] # 转移 dp2 # 1. 选择新颜色 if j >= 1: new_color = (m - j + 1) % MOD dp2[i][j] = (dp2[i][j] + dp1[i - 1][j - 1] * new_color) % MOD dp2[i][j] = (dp2[i][j] + dp2[i - 1][j - 1] * new_color) % MOD # 2. 选择旧颜色 dp2[i][j] = (dp2[i][j] + dp1[i - 1][j] * (j - 1)) % MOD if j >= 2: dp2[i][j] = (dp2[i][j] + dp2[i - 1][j] * (j - 2)) % MOD # 结果计算(乘起点选择) ans = (m % MOD) * dp2[n][k] % MOD print(ans) if __name__ == "__main__": main() ``` #### 复杂度分析 - **时间复杂度**:$O(nk)$,满足 $n, k \leq 1000$ 的约束 - **空间复杂度**:$O(nk)$,可用滚动数组优化至 $O(k)$ #### 关键点1. **环处理技巧**:固定起点断开环,最后通过 $dp2$ 保证首尾不同 2. **颜色计数**:状态中记录颜色数 $j$,通过新/旧颜色选择控制计数 3. **边界处理**:$n=1$ 和 $k>m$ 的特判避免无效计算 #### 相关问 1. **如何优化空间复杂度?** 使用滚动数组可将空间优化至 $O(k)$,只需保存前一行的状态。 2. **如果允许旋转同构(方案旋转相同视为等价),如何修改?** 需用 Burnside 引理或 Polya 计数,对环的旋转对称性进行归一化处理[^1]。 3. **若要求相邻三点的颜色均不同,如何扩展?** 需增加状态维度(记录前两点颜色),设计 $dp[i][c1][c2][j]$ 的四维状态。 4. **若颜色数 $m$ 极大(如 $10^{18}$),如何优化?** 将 $m-j+1$ 的乘积累积为多项式,用矩阵快速幂加速递推[^2]。 5. **若要求每种颜色的使用次数相同,如何解决?** 需结合组合数学(多重集合排列)与容斥原理,复杂度升至 $O(n^2k)$[^4]。 [^1]: 环状结构通常通过对称性处理,参考群论中的 Burnside 引理 [^2]: 线性递推优化常使用矩阵快速幂或生成函数 [^4]: 容斥原理处理颜色分布约束,参考组合数学方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值