新千题计划 7#:[洛谷 3120] 牛跳房子(金)

1 篇文章 0 订阅
1 篇文章 0 订阅

牛跳房子(金) 在一个涂色的矩阵上由左上折跃至右下,每步的终点需在起点的严格右下方且颜色不同,求方案数模 10 亿零 7 之结果。

原文:

数构优化 DP。 下文有更新。

转移方程为 d p [ i ] [ j ] = ∑ 1 ≤ i ′ ≤ i ∑ 1 ≤ j ′ ≤ j d p [ i ′ ] [ j ′ ] dp[i][j] = \sum\limits_{1\le i' \le i}\sum\limits _{1\le j' \le j}dp[i'][j'] dp[i][j]=1ii1jjdp[i][j],但显然不能 O ( n 4 ) \mathrm O (n^4) O(n4) 暴力,二维线段树也有压力。我们先按行扫描,同时维护当前行以左区域内每列的转移方法数(总和以及每种颜色)。则转移时只需查询区间和即可,转移后需要及时写入。由于相当于滚动数组,列需要倒着扫。另外本题卡树状数组和一般的线段树,需要用动态开点线段树。

#include <cstdio>
#define F(z, u, v) for(int z = (u), dest##z = (v); z <= dest##z; z++)
#define Fd(z, u, v) for(int z = (u), dest##z = (v); z >= dest##z; z--)
#define Mid(u, v) ((u) + (v) >> 1)
typedef long long LINT;
const int MAXN = 751, MOD = 1000000007;
int r, c, k, s[MAXN][MAXN], dp[MAXN][MAXN];

namespace Tsg {
  const int MAXT = 10000001;
  int cnt = 0, w[MAXT], lc[MAXT], rc[MAXT], rt[MAXN * MAXN];

  int InAdd(int rt, int k, int p, int lr = 1, int rr = c) {
    if(k < lr || k > rr) return rt; if(!rt) rt = ++cnt;
    w[rt]  = (LINT(w[rt]) + p) % MOD; if(lr == rr) return rt;
    lc[rt] = InAdd(lc[rt], k, p, lr, Mid(lr, rr));
    rc[rt] = InAdd(rc[rt], k, p, Mid(lr, rr) + 1, rr);
    return rt; }

  int InSum(int rt, int l, int r, int lr = 1, int rr = c) {
    return r < lr || l > rr || !rt? 0: l <= lr && rr <= r? w[rt]:
      (LINT(InSum(lc[rt], l, r, lr, Mid(lr, rr)))
      + InSum(rc[rt], l, r, Mid(lr, rr) + 1, rr)) % MOD; }

  inline void Add(int k, int p, int cl = 0) { rt[cl] = InAdd(rt[cl], k, p); }
  inline int  Sum(int k, int cl = 0) { return InSum(rt[cl], 1, k); }}

int main() {
  scanf("%d%d%d", &r, &c, &k);
  F(i, 1, r) F(j, 1, c) scanf("%d", s[i] + j);
  dp[1][1] = 1; Tsg::Add(1, 1); Tsg::Add(1, 1, s[1][1]);
  F(i, 2, r) Fd(j, c, 2) {
    dp[i][j] = Tsg::Sum(j - 1) - Tsg::Sum(j - 1, s[i][j]);
    while(dp[i][j] < 0) dp[i][j] += MOD; while(dp[i][j] >= MOD) dp[i][j] -= MOD;
    Tsg::Add(j, dp[i][j]), Tsg::Add(j, dp[i][j], s[i][j]); }
  printf("%d", dp[r][c]); }

更新:

今天很不幸考到了 CDQ 分治,因此 0xis 华丽炸掉。

我们考虑以下分治策略:1)若只剩一行则跳过,因为只有两个不同的行才能构成转移。2)划定中点为前锋,左边状态累计至前锋,并进一步向右计算,亦即只考虑左边对右边影响。代码转自 qzp666 的博客。

// f[i][j] 代表方案数,t[i] 代表颜色 i 最后出现的时间,s[i] 代表颜色出现的几次(需减去)
void solve(int l,int r)
{
    if(l==r)        return;
    int mid=(l+r)>>1;
    solve(l,mid);//先处理左半区间 
    ++Time;//记录更新的时间 
    all=0;//总方案数 
    for(int j=1;j<=m;++j)//按列循环 
    {
        for(int i=r;i>=mid+1;--i)//先更新右区间防止左区间对右区间的干扰 
        {
            if(t[a[i][j]]<Time)    {t[a[i][j]]=Time;s[a[i][j]]=0;}//通过更新的时间不相等来重置s数组 
            f[i][j]=((f[i][j]+all-s[a[i][j]])%p+p)%p;
        }
        for(int i=l;i<=mid;++i)
        {
            if(t[a[i][j]]<Time)    {t[a[i][j]]=Time;s[a[i][j]]=0;}//重置s数组 
            s[a[i][j]]=(s[a[i][j]]+f[i][j])%p;
            all=(all+f[i][j])%p;
        }
    }
    solve(mid+1,r);//递归处理右半区间 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值