3-Partition 问题
这是算法考试的最后一题,当时匆匆写了个基于 Subset Sum 的解法,也没有考虑是否可行。
问题描述如下:
给定 \(n\) 个正整数 \(a_1 \dots a_n\) ,设下标的整数集合 \(V=\{1,2,3,\dots,n\}\) , 确定是否有三个不相交的子集 \(I,J,K \sub V\) ,满足:
\[\sum_{i \in I} a_i = \sum_{j \in J}a_j = \sum_{k \in K} a_k = \frac{sum}{3}
\]其中, \(sum\) 是所有元素之和,要求复杂度是关于 \(n\) 和 \(sum\) 的多项式时间解法。
基于 Subset Sum 的解法
Subset Sum 问题:给定一个整数数组 nums 和整数 target ,问是否存在 nums 的子集,它的和为 target ,每个元素只能使用一次。
显然 Subset Sum 问题是背包问题的特殊情况,当背包问题中所有物品的价值等于体积,那么就是 Subset Sum 问题。
思路:
- 假设
subsetSum(nums, target)能够在nums找到所有和为target的子集。 - 问题等价于找到 2 个子集 \(I, J \sub V\) ,并且 \(\sum{a_i} = \sum{a_j} = sum/3\) ,那么剩下的元素必然能保证 \(\sum{a_k} = sum/3\) .
- 通过 Subset Sum 找到所有满足目标和为
sum/3的所有子集 \(\mathcal{I}\) ,即subsetSum(V, target = sum/3)。 - 对于每一个的 \(I \in \mathcal{I}\) ,令 \(V' = V - I\) ,执行
subsetSum(V', target = sum/3),如果返回值不为空,那么说明存在这样的 \(I,J,K\) 满足 3-Partition 的条件。
Subset Sum 问题的判定形式通过动态规划是十分容易解决的,难点在于找出所有这样的子集。
建议先完成下面的「输出所有 LCS 的练习」,再进行后文的阅读。
输出所有 Subset Sum
定义 dp[i, j] 表示在 nums[0, ..., i] 中不超过 j 的最大和。
转移方程为:
\begin{aligned}
& dp[i-1, j] & \text{ if } j < a_i \\
& \max(dp[i-1, j], dp[i-1, j-a_i]+a_i), & \text{ if } j \ge a_i
\end{aligned}
\right.
\]
最后结果为 dp[n, target] == target .
代码实现
vector<vector<int>> subsetSum(vector<int> &nums, int target)
{
int n = nums.size();
vector<vector<int>> dp(n + 1, vector<int>(target + 1, 0));
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= target; j++)
{
int x = nums[i - 1];
if (j >= x) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - x] + x);
else dp[i][j] = dp[i - 1][j];
}
}
// print all subsets
vector<vector<int>> result;
function<bool(int, int, vector<int>)> getSubsets = [&](int i, int j, vector<int> subset)
{
while (i >= 1 && j >= 0)
{
int x = nums[i - 1];
if (j >= x)
{
int t = dp[i - 1][j - x] + x;
if (dp[i - 1][j] > t) i--;
else if (dp[i - 1][j] < t) j -= x, i--, subset.emplace_back(x);
else
{
getSubsets(i - 1, j, subset);
subset.emplace_back(x), getSubsets(i - 1, j - x, subset);
return true;
}
}
else i--;
}
result.emplace_back(subset);
return true;
};
if (dp[n][target] == target)
{
getSubsets(n, target, vector<int>{});
return result;
}
return {};
}
int main()
{
vector<int> nums = {1, 3, 7, 8, 9};
int t = 16;
auto result = subsetSum(nums, t);
for (auto &v : result)
{
for (int x : v) cout << x << ' ';
cout << endl;
}
}
3-Partition
基于上述的 Subset Sum ,我们可以写出 3-Partition 的代码。
bool threePartition(vector<int> &nums)
{
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 3 != 0) return false;
int target = sum / 3;
auto subsets = subsetSum(nums, target);
for (auto &I : subsets)
{
vector<int> buf(nums.size() - I.size());
// buf = nums - I
auto itor = set_symmetric_difference(nums.begin(), nums.end(), I.begin(), I.end(), buf.begin());
buf.resize(itor - buf.begin());
if (subsetSum(buf, target).size() != 0) return true;
}
return false;
}
int main()
{
vector<int> nums = {1, 2, 3, 4, 4, 5, 8};
cout << threePartition(nums) << endl;
}
Subset Sum 的时间复杂度为 \(O(nt)\),而此处 t = sum/3 ,因此 3-Partition 的时间复杂度为 \(O(kn \cdot sum)\) ,\(k\) 是 集合 \(I\) 的个数,显然,\(k\) 有可能是指数级别的。
显然,基于上述操作,我们同样能找到所有满足条件的 \(I,J,K\) .
动态规划
上面是比较容易想到的思路,但这里的 3-Partition 是一个判定问题,我们只需要给出 YES or NO,而不需要给出具体的 \(I,J,K\) ,因此用动态规划可以使问题变得简单。
2-Partition
先考虑 Subset Sum 的一种特殊情况:给定 nums ,问是否存在一个子集 I , 使得 sum(I) = sum(nums) / 2 .
其实换汤不换药。
int twoPartition(vector<int> &nums)
{
int sum = accumulate(nums.begin(), nums.end(), 0);
int n = nums.size();
if (sum % 2 != 0) return false;
// dp[i, j] 表示前 j 个数字中,是否存在一个和为 i 的子集(允许为空集)
vector<vector<int>> dp(sum / 2 + 1, vector<int>(n + 1, false));
for (int i = 0; i <= n; i++) dp[0][i] = true;
for (int i = 1; i <= sum / 2; i++) dp[i][0] = false;
for (int i = 1; i <= sum / 2; i++)
{
for (int j = 1; j <= n; j++)
{
int x = nums[j - 1];
if (i >= x) dp[i][j] = dp[i][j - 1] || dp[i - x][j - 1];
else dp[i][j] = dp[i][j - 1];
}
}
return dp[sum / 2][n];
}
3-Partition
定义 dp[j, k] 表示:在 nums[1, ..., n] 中,是否存在一个子集,使得它的和为 j ;同时存在另外一个不相交子集,它的和为 k .
注意这里的前提条件是,在 [1, ..., n] 这个范围,而且 dp[j, k] = true 当且仅当 2 个子集同时存在。
那么最后的答案是 dp[sum / 3, sum / 3] .
转移方程为:
\]
类似于自顶向下的填表顺序,转移方程可以改写为:
\]
代码实现
int threePartition(vector<int> &A)
{
int sum = accumulate(A.begin(), A.end(), 0);
int size = A.size();
if (sum % 3 != 0) return false;
vector<vector<int>> dp(sum + 1, vector<int>(sum + 1, 0));
dp[0][0] = true;
// process the numbers one by one
for (int i = 0; i < size; i++)
{
for (int j = sum; j >= 0; j--)
{
for (int k = sum; k >= 0; k--)
{
if (dp[j][k])
{
dp[j + A[i]][k] = true;
dp[j][k + A[i]] = true;
}
}
}
}
return dp[sum / 3][sum / 3];
}
输出所有 LCS
输出一个 LCS 可以参考:https://www.cnblogs.com/sinkinben/p/14536604.html
思路很简单,在填 dp 表的过程中,转移路径实际上就是记录了 LCS 的结果,我们只需要通过回溯法,找到所有 (alen, blen) => (1, 1) 的路径即可。

代码实现
int lcs(const string &a, const string &b)
{
int alen = a.length(), blen = b.length();
vector<vector<int>> dp(alen + 1, vector<int>(blen + 1, 0));
for (int i = 1; i <= alen; i++)
{
for (int j = 1; j <= blen; j++)
{
if (a[i - 1] == b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
// print all lcs
function<void(int, int, string)> printlcs = [&](int i, int j, string str)
{
while (i >= 1 && j >= 1)
{
if (a[i - 1] == b[j - 1])
str.push_back(a[i - 1]), i--, j--;
else
{
if (dp[i - 1][j] > dp[i][j - 1]) i--;
else if (dp[i - 1][j] < dp[i][j - 1]) j--;
else
{
printlcs(i - 1, j, str);
printlcs(i, j - 1, str);
return;
}
}
}
reverse(str.begin(), str.end());
result.insert(str);
};
printlcs(alen, blen, "");
for (auto &x : result) cout << x << endl;
return dp[alen][blen];
}
int main()
{
// string a = "cnblog", b = "belong";
string a = "xyxxzxyzxy", b = "zxzyyzxxyxxz";
// string a = "ABCBDAB", b = "BDCABA";
cout << lcs(a, b) << endl;
}
3-Partition 问题的更多相关文章
- Partition:增加分区
在关系型 DB中,分区表经常使用DateKey(int 数据类型)作为Partition Column,每个月的数据填充到同一个Partition中,由于在Fore-End呈现的报表大多数是基于Mon ...
- Partition:Partiton Scheme是否指定Next Used?
在SQL Server中,为Partition Scheme多次指定Next Used,不会出错,最后一次指定的FileGroup是Partition Scheme的Next Used,建议,在执行P ...
- Partition:分区切换(Switch)
在SQL Server中,对超级大表做数据归档,使用select和delete命令是十分耗费CPU时间和Disk空间的,SQL Server必须记录相应数量的事务日志,而使用switch操作归档分区表 ...
- sql 分组取最新的数据sqlserver巧用row_number和partition by分组取top数据
SQL Server 2005后之后,引入了row_number()函数,row_number()函数的分组排序功能使这种操作变得非常简单 分组取TOP数据是T-SQL中的常用查询, 如学生信息管理系 ...
- Oracle Partition Outer Join 稠化报表
partition outer join实现将稀疏数据转为稠密数据,举例: with t as (select deptno, job, sum(sal) sum_sal from emp group ...
- SQLServer中Partition By 函数的使用
今天群里看到一个问题,在这里概述下:查询出不同分类下的最新记录.一看这不是很简单的么,要分类那就用Group By;要最新记录就用Order By呗.然后在自己的表中试着做出来: 首先呢我把表中的数据 ...
- [LeetCode] Partition Equal Subset Sum 相同子集和分割
Given a non-empty array containing only positive integers, find if the array can be partitioned into ...
- [LeetCode] Partition List 划分链表
Given a linked list and a value x, partition it such that all nodes less than x come before nodes gr ...
- 快速排序中的partition函数的枢纽元选择,代码细节,以及其标准实现
很多笔试面试都喜欢考察快排,叫你手写一个也不是啥事.我很早之前就学了这个,对快速排序的过程是很清楚的.但是最近自己尝试手写,发现之前对算法的细节把握不够精准,很多地方甚至只是大脑中的一个映像,而没有理 ...
- [bigdata] kafka基本命令 -- 迁移topic partition到指定的broker
版本 0.9.2 创建topic bin/kafka-topics.sh --create --topic topic_name --partition 6 --replication-factor ...
随机推荐
- training11.14
7-10 关于堆的判断 (25分) 题目:将一系列给定数字顺序插入一个初始为空的小顶堆H[].随后判断一系列相关命题是否为真.命题分下列几种: x is the root:x是根结点: x and ...
- OO随笔之纠结的第二单元——多线程电梯
综述 主要任务就是写一个电梯模拟器,读入每一个人的请求然后让电梯把他们送到想去的地方. 从第一次到第三次作业,三次的主要任务都是相同的,但是每次都增加了很多的细节,每次的难度都逐步增长,电梯复杂度和瞎 ...
- .NET平台系列8 .NET Core 各版本新功能
系列目录 [已更新最新开发文章,点击查看详细] .NET Core 自2016年6月27日发布第一个正式版本以来,它主打的跨平台和高性能特效吸引了许多开发者,包括Java.PHP等语言的开发者 ...
- Build 2021 :正式发布.NET 6 Preview4
微软在不断推进.NET 6的可用性,昨晚的Build 2021大会上发布了Preview4, 这是一个很大的版本更新,带来大量的功能,以及接近最终的产品交付质量,不过,这并不意味着可以在生产环境使用了 ...
- [bug] redis-cli连接时出现Could not connect to Redis at 127.0.0.1:6379: Connection refused
参考 https://www.geek-share.com/detail/2684728161.html
- [BD] Sqoop
什么是Sqoop 数据交换工具(ETL):RDBMS(MySQL.Oracle等)<-->Sqoop<-->HDFS(HBase.Hive等) 基于JDBC 执行数据交换时,本 ...
- 在 Linux 中,最直观、最可见的部分就是 文件系统(file system)
在 Linux 中,最直观.最可见的部分就是 文件系统(file system).下面我们就来一起探讨一下关于 Linux 中国的文件系统,系统调用以及文件系统实现背后的原理和思想.这些思想中有一些来 ...
- 【odoo】【知识点】视图的继承逻辑
背景:同一个模块,两组开发人员对同一个模型的form视图进行了二开.在没有指定外部ID的情况下,odoo是如何选择展示展示哪个视图呢? 上干货 odoo在加载视图的时候,首先调用的models.py中 ...
- Linux(CentOS7)下二进制安装MySQL5.7.26
记录一下自己在 CentOS7 下二进制安装 MySQL5.7.26 的过程,之前使用 Linux(CentOS7)下rpm安装MySQL8.0.16 之后发现 rpm 方式安装不利于维护,也不利于单 ...
- Docker学习(15) Docker容器的跨主机连接
Docker容器的跨主机连接 Docker使用网桥跨主机容器连接 Docker使用Open cSwitch实现跨主机容器连接 Docker使用weave实现跨主机容器连接