[蓝桥杯]数字三角形(记忆化剪枝/DP)

蓝桥账户中心

题意

比最普通的数字三角形题目多加了一个限制  就是竖直向下的步数和向右下的步数之差<=1

思路

1<=N<=100

一开始dfs  只能通过50%的样例

#include<bits/stdc++.h>
using namespace std;
#define int long long 
#define pii pair<int,int>
#define ar2 array<int,2>
#define ar3 array<int,3>
#define ar4 array<int,4>
#define endl '\n'
void cmax(int &a,int b){a=max(a,b);}
void cmin(int &a,int b){a=min(a,b);}
const int N=200,MOD=1e9+7,INF=0x3f3f3f3f,LINF=LLONG_MAX;
int n,ans=0;
int g[N][N],dp[N][N];
bool vis[N][N];

void dfs(int layer,int idx,int l,int r,int sum){//layer上一次考虑第几层 idx表示第layer层选的是第idx个
    
    if((n-1)&1){
        if(max(l,r)>(n-1)/2+1){
            return;
        }
    }else{
        if(max(l,r)>(n-1)/2){
            return;
        }
    }
    if(layer==n){
        if(abs(l-r)<=1)
        cmax(ans,sum);
        return;
    }
    
    dfs(layer+1,idx,l+1,r,sum+g[layer+1][idx]);
    
    dfs(layer+1,idx+1,l,r+1,sum+g[layer+1][idx+1]);

}

void solve() {
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            cin>>g[i][j];
        }
    }
    
    dfs(1,1,0,0,g[1][1]);
    cout<<ans<<endl;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t=1;
    // cin>>t;
    while (t--) solve();

}

看了题解 只需要简单的dp  

观察发现 竖直向下和向右下走的步数要么相等 要么差一步 

在列方向上  只有向右下走的步数会对最终答案所在的列产生影响

一共n行  要走n-1步

①n为奇数  n-1为偶数  向下和向右各走(n-1)/2步  所以最终答案落在dp[n][(n-1)/2+1]

②n为偶数  n-1为奇数  向右走(n-1)/2或者(n-1)/2+1步 

所以最终答案对dp[n][(n-1)/2+1]和dp[n][(n-1)/2+2]取max即可

这里必须对n分类讨论 如果n为奇数 但是还用②取max的方法会wa

Code

#include<bits/stdc++.h>
using namespace std;
#define int long long 
#define pii pair<int,int>
#define ar2 array<int,2>
#define ar3 array<int,3>
#define ar4 array<int,4>
#define endl '\n'
void cmax(int &a,int b){a=max(a,b);}
void cmin(int &a,int b){a=min(a,b);}
const int N=200,MOD=1e9+7,INF=0x3f3f3f3f,LINF=LLONG_MAX;
int n;
int g[N][N],dp[N][N];

void solve() {
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            cin>>g[i][j];
        }
    }
    
    dp[1][1]=g[1][1];
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++){
            dp[i][j]=g[i][j]+max(dp[i-1][j],dp[i-1][j-1]);
        }
    }
    if(n&1) cout<<dp[n][(n-1)/2+1]<<endl;
    else cout<<max(dp[n][(n-1)/2+1],dp[n][(n-1)/2+2])<<endl;

}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t=1;
    // cin>>t;
    while (t--) solve();

}

DFS记忆化剪枝

int dp[N][N][N];  // 三维,(layer, idx, l)

void dfs(int layer,int idx,int l,int r,int sum){
    if((n-1)&1){
        if(max(l,r)>(n-1)/2+1) return;
    }else{
        if(max(l,r)>(n-1)/2) return;
    }

    if(layer==n){
        if(abs(l-r)<=1) cmax(ans,sum);
        return;
    }

    if(dp[layer][idx][l]>=sum) return; // 记忆化剪枝
    dp[layer][idx][l]=sum;

    dfs(layer+1,idx,l+1,r,sum+g[layer+1][idx]);
    dfs(layer+1,idx+1,l,r+1,sum+g[layer+1][idx+1]);
}

void solve(){
    memset(dp,-1,sizeof dp);
    ......
}

GPT帮改的记忆化 其实只加了两行代码 感觉茅塞顿开

别忘了初始化dp数组为-1 

这样就能ac了

如何确定状态?

你的原递归函数是这样的:

void dfs(int layer, int idx, int l, int r, int sum)
  • layer: 当前在第几层。
  • idx: 当前层的第几个位置。
  • l: 向左下方向走的次数。
  • r: 向右下方向走的次数。
  • sum: 到当前为止路径上累积的和。

分析状态冗余:

  • sum不需要纳入记忆化状态,因为记忆化的目的是剪枝,而不是精确存储所有的路径之和 sum就是dp数组对应的值  不需要开一维记录这个状态
  • r可以用其他变量代替:
    因为在第layer层时,已经走过的步数 = layer - 1
    所以:
    r = (layer - 1) - l;
    这样一来,r就不用存储了,因为它可用其他变量计算出来。

因此真正关键的状态只有三个:

  • layer(第几层)
  • idx(当前所在的第几个位置)
  • l(向左下走了几次)

只要这三个状态相同,那么剩余的路径情况就完全一致

也就是说 这三个状态就能唯一确定一个dp值

所以记忆化数组优化为三维:

dp[layer][idx][l]

如何用dp数组剪枝?

每次记录当前状态的sum值  dp数组不一定要存这个点上最优的值 而是随搜索过程动态变化的
如果当前状态下 之前搜过 而且之前存的值 比当前搜到的sum更大 说明之前那次搜索一定更优 
这次就没有必要搜索了

如果当前是搜索到的更大 那么更新dp值  并继续往下搜

if (dp[layer][idx][l] >= sum) return;
dp[layer][idx][l] = sum;

GPT写的记忆化剪枝模板

int dp[层数][位置数][其他状态参数];

void dfs(状态参数们, int sum) {
    if (到达终点或满足条件) {
        更新答案;
        return;
    }

    if (dp[当前状态] >= sum) return; // 记忆化剪枝
    dp[当前状态] = sum;              // 更新最佳状态值

    dfs(下一个状态, 更新的sum);
    dfs(另一个状态, 更新的sum);
}

<think>好的,我现在需要帮助用户了解矩阵计数的概念和方法。首先,我需要明确用户提到的“矩阵计数”具体指的是什么。根据提供的引用内容,尤其是引用[2]和引用[5],用户可能是在询问如何计算符合特定条件的矩阵数量,比如在限制条件下(如不允许连续三个X)的矩阵排列组合问题。此外,引用[5]提到了状压DP(动态规划)的应用,这可能是一个关键点。 首先,我应该定义矩阵计数的问题。根据引用[5],问题可能涉及生成所有合法的行,即那些不包含连续三个X的行,然后使用动态规划的方法来计算所有可能的矩阵组合。这涉及到状态压缩,因为每一行的状态可以用二进制或其他压缩形式表示,以简化计算。 接下来,我需要解释矩阵计数的核心方法。动态规划在这里的作用是逐步构建矩阵,每一行的选择依赖于前一行的状态,以避免违反条件。例如,在棋盘覆盖问题或排列问题中,每一行的选择必须满足与上一行兼容的条件。这需要设计一个状态转移方程,如引用[2]中提到的dp[i][j][k],其中i表示当前行,j表示当前行的状态,k表示前一行的状态。 然后,我需要考虑如何实现这种方法。可能需要预处理所有合法的行状态,然后构建状态转移矩阵。例如,在Python中,可以先生成所有可能的合法行,然后使用三维数组来存储状态转移的可能性,并累加有效状态的数量。 此外,用户可能对应用场景和优化方法感兴趣。例如,矩阵计数可以应用于图像处理、组合数学问题或算法竞赛中的题目(如蓝桥杯国赛题)。优化方法可能包括状态压缩以减少内存使用,或者剪枝策略来减少不必要的计算步骤。 最后,我需要确保回答符合用户的要求,使用正确的中文术语,并引用相关的参考资料。例如,引用[5]提到的蓝桥杯题目和状压DP方法,以及引用[2]中的状态转移方程,都需要在回答中正确引用,以增强可信度。 在生成相关问题部分,我需要考虑用户可能进一步询问的方向,比如动态规划的具体步骤、状态压缩的实现细节、矩阵计数的应用实例等。这些问题可以帮助用户深入理解该算法的不同方面。 总结来说,回答的结构应包括定义、核心方法、实现步骤、应用场景和优化策略,同时结合引用中的具体例子和方法,确保内容准确且易于理解。</think>### 矩阵计数的概念与方法 矩阵计数是一种通过特定规则统计满足条件的矩阵数量的算法问题。其核心通常涉及动态规划(DP)、状态压缩(状压DP)和图结构分析,尤其在处理大规模或复杂约束条件时,这些方法能显著提升效率[^5]。 --- #### **核心方法** 1. **状态压缩动态规划(状压DP)** - **定义**:将矩阵每行的状态(如是否包含连续三个`X`)编码为二进制或整数形式,压缩存储状态信息以减少计算复杂度。 - **状态转移**: 设`dp[i][j][k]`表示第`i`行的合法状态为`j`,且前一行的合法状态为`k`时,符合条件的矩阵数量。转移时需确保相邻行的状态满足约束条件[^2]。 状态转移方程示例: $$ dp[i][j][k] = \sum_{\text{所有与j兼容的前一状态k}} dp[i-1][k][l] $$ 2. **合法行预处理** - 生成所有合法行(如不包含连续三个`X`的行),并将这些行编码为状态集合。例如,通过递归或位运算遍历所有可能行,筛选出符合条件的行[^5]。 3. **动态规划实现步骤** - **初始化**:计算第一行所有合法状态的初始值(通常为1)。 - **逐行递推**:对每一行遍历所有可能的当前状态和前一行状态,累加合法组合的数量。 - **结果汇总**:最终结果为最后一行所有状态的总和。 --- #### **实现示例(Python)** 以下为蓝桥杯国赛“矩阵计数”问题的状压DP实现片段: ```python def count_matrix(n, m): # 生成所有合法行(无连续三个X) def is_valid(row): return 'XXX' not in ''.join(row) from itertools import product valid_rows = [] for bits in product(['X', '.'], repeat=m): if is_valid(bits): valid_rows.append(bits) # 状态转移 dp = [[0] * len(valid_rows) for _ in range(len(valid_rows))] for i in range(len(valid_rows)): dp[0][i] = 1 # 初始化第一行 for row in range(1, n): new_dp = [[0]*len(valid_rows) for _ in range(len(valid_rows))] for prev in range(len(valid_rows)): for curr in range(len(valid_rows)): if compatible(valid_rows[prev], valid_rows[curr]): for state in range(len(valid_rows)): new_dp[curr][prev] += dp[prev][state] dp = new_dp return sum(sum(row) for row in dp) # 检查两行是否兼容(例如上下行无冲突) def compatible(prev_row, curr_row): # 根据具体规则实现 return True ``` --- #### **应用场景** 1. **组合数学问题**:如统计满足特定布局的矩阵数量(棋盘覆盖、数独约束)。 2. **图像处理**:在密集场景中估计矩阵模式,类似人群计数中的密度图积分方法[^4]。 3. **图结构分析**:用于邻接矩阵的三角形计数,衡量图的结构特性[^1][^3]。 --- #### **优化策略** 1. **剪枝优化**:在生成合法行时提前剔除无效状态。 2. **并行计算**:对大规模问题采用分布式计算加速状态转移[^1]。 3. **内存压缩**:仅保留当前行和前一行状态,减少空间复杂度。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值