HZOJ #41.墙壁涂色

题目描述

给一个环形的墙壁涂颜色,颜色一共有 k 种,墙壁被竖直地划分成 n 个部分,相邻的部分颜色不能相同。请你写程序计算出一共有多少种给墙壁上色的方案?

例如,当 n=5,k=3​ 时,下面是一种合法的涂色方案

44934979.jpg

而由于墙壁是环形的,所以下面就是一种非法的方案

10258141.jpg


输入

输入两个数字 n,k(1≤n≤103,2≤k≤10),分别代表墙壁数量和颜色种类。

输出

对于每个询问,输出一行整数,合法的墙壁涂色方案数。


样例输入1
5 3
样例输出1
30

数据规模与约定

时间限制:5 s

内存限制:256 M

20% 的数据保证 n≤20,k=3

40% 的数据保证 n≤40,k=4

80% 的数据保证 n≤40,k≤10

100% 的数据保证 n≤103,k≤10

这道题我一开始的想法很简单:因为他只需要最后一个的颜色与第一个不一样,那么在此之前就按正常情况递推,等推到最后一个之后再分类讨论。也就是设置一个数组dp[i]存储前i个墙壁合法上色方案的总数,然后在i=n之前,dp[i]=dp[i-1]*(k-1),一直递推直到最后一个。但就是这最后一个出了问题:正常的话我的思路是dp[n]=(dp[i-1]中首尾两种颜色相等的情况)*(k-1)+(dp[i-1]中首尾两个颜色不相等的情况)*(k-2)。两种情况相互是对方的补集,算出一种就能知道另一种,但实际上,我们一个都求不出来。或者说,可能能求得出来,但一定会很麻烦,非常麻烦,只能另寻他路。

接下来介绍一个合适的解法:令dp[n][i][j]代表前n块墙壁的首尾颜色分别为i与j时的合法方案数,接着我们可以知道:

dp[n][i][j]=\sum_{l=1}{dp[n-1][i][l]} (l<=k\bigcap l!=j )

上述便是我们本题的递推公式。用人话来说就是:前n块墙壁的首尾颜色分别为i与j时的合法方案数=所有前n-1块墙壁的合法方案里去掉其尾块与当前我们要求的这种情况的尾块不相同的情况的其他合法方案累加起来的总数。

既然有题目有特殊的要求,我们干脆直接把他们加到我们的分类讨论的对象中来,虽然我们所要的答案是最后求得的答案的真子集,还需筛选,但这也不是什么问题。有时候看起来没有一步到位的方法,反而其实会更好去实现一些。

代码如下:

#include <iostream>
#include<vector>
using namespace std;

long long dp[1006][16][16];//dp[n][i][j]代表前n块墙壁的首尾颜色分别为i与j时的合法方案数

int main()
{
    int n, k;
    cin >> n >> k;
    for (int i = 1;i <= k;i++)
    {
        dp[1][i][i] = 1;//赋初值:当只有一块墙壁可涂时,只有在i,j相等的条件下才能赋值为1,其余情况都为0
    }

    for (int nn = 2;nn <= n;nn++)
    {
        for (int i = 1;i <= k;i++)
        {
            for (int j = 1;j <= k;j++)
            {
                dp[nn][i][j] = 0;
                for (int l = 1;l <= k;l++)
                {
                    if (l == j)
                        continue;
                    else dp[nn][i][j] += dp[nn - 1][i][l];
                }
            }
        }
    }
    //最后筛选答案
    int ans = 0;
    for (int i = 1;i <= k;i++)
    {
        for (int j = 1;j <= k;j++)
        {
            if (i == j) continue;
            else ans += dp[n][i][j];
        }
    }
    cout << ans << endl;

    return 0;
}

当然,这道题要完全过关还需要实现高精度运算,因为之前有提过,我这里不做赘述。当然还有一点,本题最后一个测试用例过于大,正常使用高精度会爆内存,因此我们采用一个滚动数组的技巧。即我们不难发现,上述代码中,咱们的求解dp[nn][i][j] 只和 dp[nn - 1][i][l] 有关,也就是说,实际上我们dp数组的第一个数不需要1000多,只需要2,完全可以胜任计算。

直接上代码:

#include <iostream>
#include<vector>
using namespace std;
class BigInt :public vector<int>
{
public:
    BigInt() { push_back(0); }
    BigInt(int x)
    {
        this->push_back(x);
        process_digit();
    }
    void process_digit()
    {
        for (int i = 0;i < size();i++)
        {
            if (at(i) >= 10)
            {
                if (i == size() - 1)
                    push_back(0);
                at(i + 1) += at(i) / 10;
                at(i) %= 10;
            }
            else continue;
        }
        return;
    }
    BigInt& operator+=(const BigInt& a)
    {
        for (int i = 0;i < a.size();++i)
        {
            if (i + 1 > size())
                push_back(a[i]);
            else at(i) += a[i];
        }
        process_digit();
        return *this;
    }
    BigInt operator+(const BigInt& a)
    {
        BigInt ret(*this);
        ret += a;
        return ret;
    }
};
ostream &operator<<(ostream &o,const BigInt &a)
{
    for (int i = a.size() - 1;i >= 0;i--)
    {
        o << a.at(i);
    }
    return o;
}

BigInt dp[2][16][16];//dp[n][i][j]代表前n块墙壁的首尾颜色分别为i与j时的合法方案数
//使用滚动数组技巧,防止超内存
int main()
{
    int n, k;
    cin >> n >> k;
    for (int i = 1;i <= k;i++)
    {
        dp[1][i][i] = 1;//赋初值:当只有一块墙壁可涂时,只有在i,j相等的条件下才能赋值为1,其余情况都为0
    }

    for (int nn = 2;nn <= n;nn++)
    {
        for (int i = 1;i <= k;i++)
        {
            for (int j = 1;j <= k;j++)
            {
                dp[nn % 2][i][j] = 0;//使用滚动数组。
                for (int l = 1;l <= k;l++)
                {
                    if (l == j)
                        continue;
                    else dp[nn % 2][i][j] += dp[(nn - 1) % 2][i][l];
                }
            }
        }
    }
    //最后筛选答案
    BigInt ans = 0;
    for (int i = 1;i <= k;i++)
    {
        for (int j = 1;j <= k;j++)
        {
            if (i == j) continue;
            else ans += dp[n % 2][i][j];
        }
    }
    cout << ans << endl;

    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值