AcWing 321. 棋盘分割(记忆化搜索 二维区间DP 二维前缀和)

31 篇文章 0 订阅
9 篇文章 0 订阅

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

题意:

给定一个 8×8 的棋盘,棋盘的 每个小方格 都有一个 权值 wx,y

每次我们可以对棋盘进行一次 横切竖切,将 棋盘 分成两块矩形的 子棋盘

分割完一次后,我们可以 选择两个 子棋盘 中的一个 再继续 递归 操作
在这里插入图片描述

可以发现题目中给的这个图片,右边这个并不是递归下去做的:他对第一次分割的两个矩形又分别进行了分割(题目要求我们只能保留一个继续分割

现需要把棋盘按照上述分割方案,分成 n 块(n−1 次划分操作)

求一个划分方案,使得各个子棋盘总分的均方差最小

思路:

本题如果用 动态规划 来分析,状态表示要至少开5维,记录分割操作的阶段,以及当前的棋盘状态

状态表示:
  • 集合:将左上角坐标为 (x1, y1),右上角坐标为 (x2, y2) 的子矩阵切分成 k 部分的所有方案
  • 属性:均方差(见题面中给出的公式)的最小值
状态计算:

以
第一次切分为例:

对于横切从上往下一共可有 7 种不同的位置,每个位置切分之后都会有上下部分两种选择,因此共有 14 种可能

对于纵切是类似的,从左往右一共有 7 种不同的位置,每个位置切分之后会有左右部分两种选择,因此也有 14 种可能

现假设某一类方案如下:

xi 处横切,且选择上面部分,所有这类方案都可分为两部分:

  • 继续选择上方部分切分的均方差的分值(根据定义 m i n = f ( x 1 , y 1 , i , y 2 ) min = f(x1, y1, i, y2) min=f(x1,y1,i,y2)
  • 剩余部分均方差的分值(这是个定值,可结合“二维前缀和”在O(1)的复杂度内求得)

可得上述情况①的状态转移方程: d p [ x 1 ] [ y 1 ] [ x 2 ] [ y 2 ] [ k ] = m i n ( d p [ x 1 ] [ y 1 ] [ i ] [ y 2 ] [ k − 1 ] + g e t ( i + 1 , y 1 , x 2 , y 2 ) ) dp[x1][y1][x2][y2][k] = min(dp[x1][y1][i][y2][k-1] + get(i+1, y1, x2, y2)) dp[x1][y1][x2][y2][k]=min(dp[x1][y1][i][y2][k1]+get(i+1,y1,x2,y2))

同理,我们可以进而得出以下 3 类的状态转移方程

情况②,取横切的下半部分: d p [ x 1 ] [ y 1 ] [ x 2 ] [ y 2 ] [ k ] = m i n ( d p [ i + 1 ] [ y 1 ] [ x 2 ] [ y 2 ] [ k − 1 ] + g e t ( x 1 , y 1 , i , y 2 ) ) dp[x1][y1][x2][y2][k] = min(dp[i+1][y1][x2][y2][k-1] + get(x1, y1, i, y2)) dp[x1][y1][x2][y2][k]=min(dp[i+1][y1][x2][y2][k1]+get(x1,y1,i,y2))

在这里插入图片描述

情况③,取纵切的左半部分: d p [ x 1 ] [ y 1 ] [ x 2 ] [ y 2 ] [ k ] = m i n ( d p [ x 1 ] [ y 1 ] [ x 2 ] [ j ] [ k − 1 ] + g e t ( x 1 , j + 1 , x 2 , y 2 ) ) dp[x1][y1][x2][y2][k] = min(dp[x1][y1][x2][j][k-1] + get(x1, j+1, x2, y2)) dp[x1][y1][x2][y2][k]=min(dp[x1][y1][x2][j][k1]+get(x1,j+1,x2,y2))

情况④,取纵切的右半部分: d p [ x 1 ] [ y 1 ] [ x 2 ] [ y 2 ] [ k ] = m i n ( d p [ x 1 ] [ j + 1 ] [ x 2 ] [ y 2 ] [ k − 1 ] + g e t ( x 1 , y 1 , x 2 , j ) ) dp[x1][y1][x2][y2][k] = min(dp[x1][j+1][x2][y2][k-1] + get(x1, y1, x2, j)) dp[x1][y1][x2][y2][k]=min(dp[x1][j+1][x2][y2][k1]+get(x1,y1,x2,j))

在这里插入图片描述

根据题目给出的公式:(xi - x̅) ^ 2 / n(xi 为某块二维矩形棋盘的总分)

显然,get函数的编写需要结合子矩阵求前缀和的模板:

构建子矩阵,均从原点开始

s [ x 1 ] [ y 1 ] s[x1][y1] s[x1][y1]表示:从原点(0, 0)到点(x1, y1)矩阵和

初始化

s [ x 1 ] [ y 1 ] = s [ x 1 − 1 ] [ y 1 ] + s [ x 1 ] [ y 1 − 1 ] − s [ x 1 − 1 ] [ y 1 − 1 ] + a [ i ] [ j ] s[x1][y1] = s[x1-1][y1] + s[x1][y1-1] - s[x1-1][y1-1] + a[i][j] s[x1][y1]=s[x11][y1]+s[x1][y11]s[x11][y11]+a[i][j]

所以子矩阵点(x1, y1)(x2, y2)的和为: s [ x 2 ] [ y 2 ] − s [ x 2 ] [ y 1 − 1 ] − s [ x 1 − 1 ] [ y 2 ] + s [ x 1 − 1 ] [ y 1 − 1 ] s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] s[x2][y2]s[x2][y11]s[x11][y2]+s[x11][y11]

get函数(结合二维前缀和公式)

int get_sum(int x1, int y1, int x2, int y2)
{
    return s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
}

double get(int x1, int y1, int x2, int y2)
{
    double sum = get_sum(x1, y1, x2, y2) - X;
    return (double)sum * sum / n;
}

时间复杂度:

O ( n 5 ) O(n^5) O(n5)

代码:

#pragma GCC optimize(2)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#include<bits/stdc++.h>

using namespace std;

typedef double db;
const int N = 15, M = 9;
const db inf = 1e9;

int n;
int s[M][M];//二维前缀和
db f[M][M][M][M][N];
db X;//表示平均分

int get_sum(int x1, int y1, int x2, int y2)
{
    return s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
}

db get(int x1, int y1, int x2, int y2)//含义见题解
{
    db sum = get_sum(x1, y1, x2, y2) - X;
    return (db)sum * sum / n;
}

db dfs(int x1, int y1, int x2, int y2, int k)
{
    db &v = f[x1][y1][x2][y2][k];
    if(v>=0) return v;//说明v被计算过,直接返回
    if(k==1) return v = get(x1, y1, x2, y2);//k=1说明不能再切分了,即只有一部分直接返回

    //如果上述都不符合则开始计算所有类别的最大值
    v = inf;
    for (int i=x1; i<x2; ++i)//先枚举横切(切后可以选上面部分也可以下面部分)
    {
        v = min(v, dfs(i+1, y1, x2, y2, k-1) + get(x1, y1, i, y2));//选上部,切完一刀后拿出一部分,因此剩余需要切分的部分就可以减一了(k-1)
        v = min(v, dfs(x1, y1, i, y2, k-1) + get(i+1, y1, x2, y2));//选下部
    }

    for (int j=y1; j<y2; ++j)//后枚举纵切(切后可以选左边部分也可以右边部分)
    {
        v = min(v, dfs(x1, y1, x2, j, k-1) + get(x1, j+1, x2, y2));//选左边
        v = min(v, dfs(x1, j+1, x2, y2, k-1) + get(x1, y1, x2, j));//选右边
    }

    return v;
}

int main()
{
    cin>>n;
    for (int i=1; i<=8; ++i)
        for (int j=1; j<=8; ++j)
        {
            cin>>s[i][j];
            s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
        }

    X = (db)s[8][8] / n;// 总和/n,n是最终分得的份数
    memset(f, -1, sizeof f);
    printf("%.3lf\n", sqrt(dfs(1, 1, 8, 8, n)));//为了方便,我们在最后才加上根号

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值