首页 > 其他分享 >「线性DP」乘积最大

「线性DP」乘积最大

时间:2023-03-20 18:59:13浏览次数:50  
标签:乘积 高精度 int DFS leq num DP 线性 dp

本题为3月20日23上半学期集训每日一题中A题的题解

题面

题目描述

今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰90周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友XZ也有幸得以参加。活动中,主持人给所有参加活动的选手出了这样一道题目:

设有一个长度为N的数字串,要求选手使用K个乘号将它分成K+1个部分,找出一种分法,使得这K+1个部分的乘积最大。

同时,为了帮助选手能够正确理解题意,主持人还举了如下的一个例子:

有一个数字串:312, 当N=3,K=1时会有以下两种分法:

  1. \(3 \times 12 = 36\)
  2. \(31 \times 2 = 62\)

这时,符合题目要求的结果是:\(31 \times 2 = 62\) 。

现在,请你帮助你的好朋友XZ设计一个程序,求得正确的答案。

输入

第一行共有2个自然数N,K( \(4\leq N\leq 40,1\leq K\leq 6\) )

第二行是一个长度为N的数字串。

输出

输出所求得的最大乘积(一个自然数)。

样例输入

4  2
1231

样例输出

62

思路分析

这题看完题目之后我的第一反应是直接二进制枚举或者DFS,然后看了眼数据量,时间应该是不够的(实际上这题给的数据量是假的,真正的数据量很小,直接这么写应该也能过).不过可以发现此题是具有最优子结构的性质的,所以显然可以把DFS改成动态规划来进行求解.

那如何进行状态转移呢?我们先从DFS的角度来想.如果此题我们采用DFS的思路解题,我们的思路应该是类似下面这样的:

  1. 遍历所有能放乘号的间隔,以这个间隔把整个数列成两半
  2. 对左边那一半(你愿意也可以对右边那一半,但是这样改成动态规划后循环要倒着来,不推荐坑自己)递归调用此函数,即递归地求出左边插入剩下数量的乘号后的最大值(为了保证左半段能放下剩下的乘号,左半段的间隔数必须大于等于剩下的乘号数)
  3. 将递归算出的左半边乘上右半边,即是当前划分方案的值(为了保证右半边有数,所以左半边的长度最大只能是当前长度减一)
  4. 取所有划分方案的最大值

下面是上述思路的简单伪实现:

// i为需要插入的乘号数量,j为当前最后一个元素的下标(这个参数此处可以省略,但是不推荐,因为动态规划里要用到),num为当前的数,数据类型仅为代号
int count(int i, int j, string num) {
    int res = 0;
    for (int k = i - 1; k < j; k++) { // 切分后左半段最后一个元素下标,从剩下乘号数遍历到倒数第二个数
        res = max(res, count(i - 1, k, /*num[0:k](指num从0到i连起来的字符串(切片),为了方便阅读,这里包括边界)*/) * /*int(num[k + 1:j])*/);
    }
    return res;
}

DFS改成动态规划只需要用一个数组来代替递归即可:

  • 首先,数组的下标对应函数的参数,数组中存放函数在不同参数情况下的返回值;
  • 接着,我们把递下去的过程去掉,只保留传回来的过程(即递归改循环);
  • 最后我们需要的状态转移方程,就是把递归调用改成取数组中的元素即可.

所以此题可得如下状态转移方程(为方便阅读,切片包含头尾):

\(dp[i][j] = \begin{cases} 0, j \leq i - 1 (代码里偷懒没体现这一点,因为初始值我全部赋了0)\\ num[0:j], i==0\\ max_{i - 1 \leq k < j}(dp[i - 1][k] * num[k + 1:j]), 0 < i < n \end{cases}\)

(上述这种从DFS角度思考动态规划的方法,被y总(NOI金牌选手,报送北大,ACWing创始人)总结为闫氏dp分析法,他本人认为这种分析方法本质上是从集合角度出发的思考.这种思考方法非常有效,可以在遇到一些较难分析状态转移的时候尝试)

dp问题就是状态的表示和计算,表示就是集合+属性,计算就是集合的划分,要不重不漏,怎么划分呢,寻找最后一个不同点.---闫学灿

所以这题的代码只要把上述的状态转移方程实现成一个程序即可,最后一个状态即为答案.

题目里说 \(n \leq 40\) ,这个数据大小是超过C++最大的数据类型的(int128),所以必须使用高精度算法.但实际上测试下来,直接用int类型也能存,这个n实际上可能封顶是9(诈骗行为,建议连夜下载国家反诈中心app),所以单单就AC来说,不需要用到高精度算法,直接算就行.由于我个人做高精度题目都是用Python的(Python会被卡的题目就交给队友),所以这里给出的C++代码仅为AC代码,实际上需要将数值的乘改为高精度乘法.

参考代码

C++版本(仅AC)

时间复杂度: \(O(N^3K)\) (计入字符串切片使用时间,可以通过先计算出所有切片来优化一个N)

空间复杂度: \(O(NK)\)

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

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int n, m;
    cin >> n >> m;
    string num;
    cin >> num;

    vector<vector<int>> dp(m + 1, vector<int>(n, 0)); // 第一维为乘号数,第二维为当前末下标

    // 维护初始状态
    dp[0][0] = num[0] - '0';
    for (int i = 1; i < n; i++) {
        dp[0][i] = dp[0][i - 1] * 10 + num[i] - '0'; // 这里能直接推就不建议用substr,当前你也可以再维护一个二维数组,求出所有substr.
    }

    // 状态转移
    for (int i = 1; i <= m; i++) { // 乘号数
        for (int j = 0; j < n; j++) { // 当前末下标
            for (int k = i - 1; k < j; k++) { // 乘号放置位置(切分后左半段最后一个元素下标)
                dp[i][j] = max(dp[i][j],
                               dp[i - 1][k] * stoi(num.substr(k + 1, j - k)));
            }
        }
    }

    cout << dp[m][n - 1] << "\n";
    return 0;
}

python版本

如果此题的n能到达40,那么在C++中是存放不了的,需要使用高精度算法.而对时间要求不高的地方,可以用python来编写需要用到高精度算法的代码.py原生支持无限大的整型(真·无限大,想存多大存多大),同时也原生具有无损浮点数类型的支持,所以很适合在对时间要求不高的地方拿来代替高精度算法,而且写起来很快,同时也不容易出现错在高精度上(抄错板子)导致查错查半天的情况.

时间复杂度: \(O(N^3K)\) (计入字符串切片使用时间,可以通过先计算出所有切片来优化一个N)

空间复杂度: \(O(NK)\)

#!/usr/bin/env python3
# coding=utf-8

n,m = map(int,input().split())  # 输入n,m
s = input()  # 输入数字字符串

dp = [[0] * n for i in range(0,m + 1)]  # dp用二维数组,注意二维不能用乘,必须用行for来生成

dp[0] = [int(s[:i]) for i in range(1,n + 1)]  # 初始化初始状态

for i in range(1,m + 1):  # 乘号数量
    for j in range(0,n):  # 终点下标
        for k in range(i - 1,j):  # 乘号位置
            dp[i][j] = max(dp[i][j], dp[i - 1][k] * int(s[k + 1:j + 1]))  # 状态转移

print(dp[m][n-1])  # 输出

"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德

标签:乘积,高精度,int,DFS,leq,num,DP,线性,dp
From: https://www.cnblogs.com/geministar/p/zstu23_3_20_A.html

相关文章

  • 【洛谷】P4590 [TJOI2018]游园会(dp套dp)
    原题链接题意对于一个长度为\(n\)的仅由\(N,O,I\)组成且不包含字串\(NOI\)的字符串\(S\),其与一个给定的长度为\(K\)的字符串的最长公共子序列为\(LCS\)。求出......
  • wordpress外贸站,采用astra ,woocommerce,weglot等
              翻译搜索复制......
  • [线筛|欧拉筛]线性筛选素数
    来源:模板题目描述:用线行筛筛选素数,将指定范围的素数找出,达到O(n)的效果。思路时间复杂度0(n)思路任意值必然可以被分解为:​​a=b1^c1*b2*c2*...​​​例如​​9=3^3,15=3*5,......
  • #创作者激励#[触觉智能RK3568]修改屏幕 DPI(像素密度)
    【本文正在参加2023年第一期优质创作者激励计划】(目录)触觉智能RK3568购买链接如下:https://item.taobao.com/item.htm?spm=4645b.1.14.1.5c4a4a7dv1soeZ&id=6587890390......
  • go 实现udp通信
    go实现udp通信 udp:不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。服务端实现代码:......
  • 【洛谷】P2150 [NOI2015] 寿司晚宴(状压dp+根号分治)
    原题链接题意有序列\(2,3,4\cdotsn\),对于序列中的每一个数,它可以被放入两个集合中的任意一个,或者不选。最后需要满足两个集合间的数两两互质(集合内部的数不需要满足互......
  • 树形dp注意事项
    1.树形dp的for循坏能优化就优化,比如取j=min(size[x],m),k<=min(size[x],m)之类的,否则很容易TLE2.要考虑清楚不合法状态是否会对答案产生影响,如果有就要memset(dp,-1,s......
  • 基础dp
    珂爱的dp们区间dp在\(dp\)的状态设计中,设计以区间为状态的\(dp\)或以区间为阶段进行的\(dp\)即为区间\(dp\),一般有最值问题和计数问题,一般方程为\[f[l][r][.........
  • 能不能说一说 TCP 和 UDP 的区别?
    TCP是一个面向连接的、可靠的、基于字节流的传输层协议。而UDP是一个面向无连接的传输层协议。和 UDP 相比,TCP有三大核心特性:面向连接。所谓的连接,指的是客户端和服......
  • Leetcode 5.最长回文子串(区间dp)
    题目链接在这里:5.最长回文子串-力扣(LeetCode)首先肯定是个n^2的算法,枚举起点也是必要的,但是枚举终点很显然不行,但是考虑到回文串会向下兼容,因此我们可以枚举长度,这就是......