对于白书内容的学习,知道了这一类模型应该都是DAG模型。
有向无环图上的动态规划是学习动态规划的基础。很多问题都可以转化为DAG上的最长路和最短路或计数问题。
问题分析:(以矩形嵌套为例)
矩形之间的“可嵌套”关系是一个典型的二元关系,二元关系可以用图来建模。如果矩形X可以嵌套在Y里面,那么从X到Y就有一条有向边。这个图是无环的,因为一个矩形无法嵌套在自己内部。换句话说,它是一个DAG。这样,所要求的便是DAG上的最长路径。
对于DAG最长(短)路,有两种“对称”的状态定义方式:
仔细分析上面的两种状态定义方式,状态1是一种自顶向下考虑问题的方式。这种考虑问题的角度比较像递归,可以用记忆化搜索实现。他的求解是自底向上。状态2的考虑问题的方式就是它的求解方式,自底向上的考虑并求解。
这也说明了,递归求解和dp求解的区别。前者自顶向下,后者自底向上。
最长上升子序列问题,定义就不说了。需要弄清序列和字串的关系。无非就是在相对顺序不变的情况下。前者不连续后者连续。
题目:[Longest Increasing Subsequence]
问题分析
上升依托于小于关系,小于关系是一个典型的二元关系。可以用图来建模。对于数组中任意两个元素arr[i]与arr[j]( i < j),如果arr[i] < arr[j],那么他们之间存在一条边。又因为自己无法小于自己。所以不存在环。问题规约为DAG上的最长路。需要注意的是 i<j 的相对顺序不能变。并不是任意两个元素之间只要存在小于关系就有边。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int sz = nums.size();
if(!sz) return 0;
std::vector<int> dp(sz, int());
dp[0] = 1; // dp[i]表示以i结尾的最长路 dp[i] = max( dp[j] + 1, (j,i) in E )
int max = dp[0];
for(int i = 1; i < sz; ++i){
dp[i] = 1;
for(int j = 0; j < i; ++j){
if(nums[j] < nums[i]) // (j,i) in E
dp[i] = std::max(dp[j]+1, dp[i]);
}
max = std::max(dp[i], max);
}
return max;
}
};
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int sz = nums.size();
if(!sz) return 0;
std::vector<int> dp(sz, int());
dp[sz-1] = 1; // dp[i]表示从i出发的最长路。dp[i] = max( dp[j] + 1, (i,j) in E )
int max = dp[sz-1];
for( int i = sz-2; i >=0; --i ){
dp[i] = 1;
for(int j = i + 1; j < sz; ++j){
if( nums[i] < nums[j] ) // (i,j) in E
dp[i] = std::max( dp[i], dp[j] + 1 );
}
max = std::max(dp[i], max);
}
return max;
}
};
题目:[拦截导弹]
最长下降子序列。其实没区别,都是二元关系。二元关系用图建模。还是DAG模型。可以两种办法。我只采用dp建议的方式,自底向上。
/*
测试用例:
8
186 186 150 200 160 130 197 220
*/
#include
#include
#include
//#define LOCAL
const int maxn = 100;
int arr[maxn];
int dp[maxn]; // dp[i]表示以i结尾的最长路 dp[i] = max( dp[j] + 1, (j,i) in E )
int cal_lis( int* a, int n );
int main( void ){
#ifdef LOCAL
std::ifstream cin( "input.dat" );
#endif
int n = 0;
while(std::cin >> n){
for( int i = 0; i < n; ++i ){
std::cin >> arr[i];
}
int ans = cal_lis( arr, n );
std::cout << ans << std::endl;
}
#ifdef LOCAL
cin.close();
#endif
return 0;
}
int cal_lis( int* a, int n ){
dp[0] = 1;
int max = dp[0];
for( int i = 1; i < n; ++i ){
dp[i] = 1;
for( int j = 0; j < i; ++j ){
if( a[j] >= a[i] ) // (j,i) in E
dp[i] = std::max(dp[i], dp[j]+1);
}
max = std::max( dp[i], max );
}
return max;
}
题目:[合唱队形]
注意状态的定义,这个题对于lis和lds不是说用任意的一种状态定义都可以解决。而是分别必须用固定的状态才可以解决。
#include
#include
#include
//#define LOCAL
const int maxn = 100;
int arr[maxn];
int dp1[maxn]; // dp1[i]表示以i结尾的最长路
int dp2[maxn]; // dp2[i]表示从i开始的最长路
int cal_lis( int* a, int n );
int cal_lds( int* a, int n );
int main( void ){
#ifdef LOCAL
std::ifstream cin( "input.dat" );
#endif
int n = 0;
while(std::cin >> n){
for( int i = 0; i < n; ++i ){
std::cin >> arr[i];
}
cal_lis( arr, n );
cal_lds( arr, n );
int max = dp1[0] + dp2[0];
for( int i = 0; i < n; ++i ){
max = std::max( max, dp1[i] + dp2[i] );
}
std::cout << n-max+1 << std::endl;;
}
#ifdef LOCAL
cin.close();
#endif
return 0;
}
int cal_lis( int* a, int n ){
dp1[0] = 1;
int max = dp1[0];
for( int i = 1; i < n; ++i ){
dp1[i] = 1;
for( int j = 0; j < i; ++j ){
if( a[j] < a[i] ) // (j,i) in E
dp1[i] = std::max(dp1[i], dp1[j]+1);
}
max = std::max( dp1[i], max );
}
return max;
}
int cal_lds( int* a, int n ){
dp2[n-1] = 1;
int max = dp2[n-1];
for( int i = n-2; i >= 0; --i ){
dp2[i] = 1;
for( int j = i+1; j < n; ++j ){
if( a[i] > a[j] ) // (j,i) in E
dp2[i] = std::max(dp2[i], dp2[j]+1);
}
max = std::max( dp2[i], max );
}
return max;
}
题目:[nyoj-16]
这个题目需要说明以下。说它时DAG模型没有任何问题。
最上面的思路已经分析了。“可嵌套关系”可以看做是存在一条边的关系。这样原问题构成了DAG模型上的最长路问题。
但是这个题目和LIS有不同,并且很容易混淆。
LDS和LIS都是以及矩形嵌套都是DAG模型,但是前两道本质时同一个问题。但是矩形嵌套和他们是不一样的。
用上述的DP方法无法正确得到正确答案。
当然,其实对于矩形嵌套,简单的一中考虑就是把它当做数字。然后不就是一个LIS问题了嘛?有什么不一样。下面来说一说。
对于LIS和LDS,他们的序列给出之后,顺序是不能变的。比如,[4,1,2,5]这样的序列。最长路只能是1,2,5。但是,如果我要这么问呢?请选出几个数,使得他们的序列时最长路。那你可以选,1,2,4,5。这样最长路是4。当然,如果问题变成这样,那也就失去意义了。对于一个序列,你排序就好了。然后返回序列长度即可。
但是,对于矩形嵌而言则不一样,因为他们并不是和数字一样可以完全比大小。对于数字而言,3和4。要么小于要么大于,但是对于矩形(1,4)和(3,2)而言,不存在这样的一种非黑即白的关系。对于数字而言,4不小于3,那就以意味着3小于4。但是,上面的两个矩形,是互相都无法嵌套的。它是这样的一种性质。
所以,才导致了问题和LIS不完全一样。可以改变原序列当中元素的位置,来获得最长路!此时,我觉得用memo做就挺好的。思路清晰嘛!
#include
#include
#include
#include
//#define LOCAL
struct Rec
{
int a_;
int b_;
Rec( int a = 0, int b = 0 ) : a_(a), b_(b)
{}
bool operator<( const Rec& rhs ) const
{
return ( a_ < rhs.a_ && b_ < rhs.b_ )||( a_ < rhs.b_ && b_ < rhs.a_ );
}
};
int dfs( int i, std::vector< std::vector<int> > edges )
{
int n = edges.size();
int ans = 0;
for( int j = 0; j < n; ++j )
{
if( edges[i][j] )
ans = std::max( ans, dfs(j, edges) + 1 );
}
return ans;
}
int main( void )
{
#ifdef LOCAL
std::ifstream cin("input.dat");
#endif
int t = 0;
std::cin >> t;
while(t--)
{
int n = 0;
std::cin >> n;
std::vector Rec_vec;
for( int i = 0; i < n; ++i )
{
int a, b;
std::cin >> a >> b;
Rec r(a, b);
Rec_vec.push_back(r);
}
std::vector< std::vector<int> > edges( n, std::vector<int>(n, int()) );
for( int i = 0; i < n; ++i )
{
for( int j = 0; j < n; ++j )
{
if(i != j && Rec_vec[i] < Rec_vec[j] )
edges[i][j] = 1;
}
}
int ans = 0;
for( int i = 0; i < n; ++i )
{
ans = std::max( ans, dfs(i, edges) );
}
std::cout << ans+1 << std::endl;
}
#ifdef LOCAL
cin.close();
#endif
return 0;
}
在dfs的基础上加入记忆化过程即可。
#include
#include
#include
#include
//#define LOCAL
struct Rec
{
int a_;
int b_;
Rec( int a = 0, int b = 0 ) : a_(a), b_(b)
{}
bool operator<( const Rec& rhs ) const
{
return ( a_ < rhs.a_ && b_ < rhs.b_ )||( a_ < rhs.b_ && b_ < rhs.a_ );
}
};
int dfs( int i, std::vector< std::vector<int> > edges, std::vector<int>& dp )
{
// 有记忆
if(dp[i] > -1)
return dp[i];
// 无记忆
int n = edges.size();
dp[i] = 0;
for( int j = 0; j < n; ++j ){
if( edges[i][j] == 1 )
dp[i] = std::max( dp[i], dfs(j, edges, dp) + 1 );
}
return dp[i];
}
int main( void )
{
#ifdef LOCAL
std::ifstream cin("input.dat");
#endif
int t = 0;
std::cin >> t;
while(t--)
{
int n = 0;
std::cin >> n;
std::vector Rec_vec;
for( int i = 0; i < n; ++i )
{
int a, b;
std::cin >> a >> b;
Rec r(a, b);
Rec_vec.push_back(r);
}
std::vector< std::vector<int> > edges( n, std::vector<int>(n, int()) );
for( int i = 0; i < n; ++i )
{
for( int j = 0; j < n; ++j )
{
if(i != j && Rec_vec[i] < Rec_vec[j] )
edges[i][j] = 1;
}
}
int ans = 0;
std::vector<int> dp(n, -1);
for( int i = 0; i < n; ++i )
{
ans = std::max( ans, dfs(i, edges, dp) );
}
std::cout << ans+1 << std::endl;
}
#ifdef LOCAL
cin.close();
#endif
return 0;
}
memo做的时候要注意,本质还是深搜的思路。
所以,用递归实现。自顶向下的思路。
采用状态1实现。
#include
#include
#include
#include
#include
//#define LOCAL
#define N 1000 + 1
struct element{
int a_;
int b_;
element(){ std::memset(this, 0, sizeof(element)); }
element( int a, int b ) : a_(a), b_(b) {}
bool operator<( const element& rhs ) const {
return (this->a_ < rhs.a_ && this->b_ < rhs.b_)||(this->a_ < rhs.b_ && this->b_ < rhs.a_);
}
};
element arr[N];
int dp[N]; // dp[i]表示从i出发的最长路 dp[i] = max{ dp[j] + 1 | (i,j) in E }
int graph[N][N];
void create_graph( int n );
int dfs( int i, int n );
int main( void ){
#ifdef LOCAL
std::ifstream cin("input.dat");
#endif
int t = 0;
std::cin >> t;
while(t--){
int n = 0;
std::cin >> n;
for( int i = 0; i < n; ++i ){
int a,b;
std::cin >> a >> b;
arr[i].a_ = a;
arr[i].b_ = b;
}
create_graph(n);
std::memset(dp, 0, sizeof(dp));
int max = dfs(0, n);
for(int i = 1; i < n; ++i){
max = std::max( dfs(i, n), max );
}
std::cout<std::endl;;
}
#ifdef LOCAL
cin.close();
#endif
return 0;
}
void create_graph( int n ){
for( int i = 0; i < n; ++i ){
for( int j = 0; j < n; ++j ){
graph[i][j] = (arr[i] < arr[j])?1:0;
}
}
}
int dfs( int i, int n ){
if( dp[i] > 0 )
return dp[i];
else{
dp[i] = 1;
for( int j = 0; j < n; ++j ){
if( graph[i][j] )
dp[i] = std::max(dp[i], dfs(j, n) + 1);
}
return dp[i];
}
}
我又学习了其他的方法,确实,问题分析和我想的一样。虽然和LIS都是DAG模型。但是,前者的DP方法。无法直接应用。用传统的memo即可。
不过我看到了一种新的结题思路,原文链接[矩形嵌套]
如果只需要求得最多可以嵌套多少个矩形,而不要求输出序列,定义一个结构体,内含有变量a,b,输入时保证a>b(a为长,b为宽)对a进行排序,最后求b的最长上升子序列(状态转移时要加上A[j].a
转化了问题。好方法。不过,可以看出。问题和我的分析是一致的,正因为不要求元素保证原来位置不变。这样才可以进行排序。
我之所以没有想到这个办法是因为,对于最初的序列。题目的意思是找出尽可能多的,由于矩阵嵌套又不是非黑即白的关系,所以不存在按大小排序之后全是lis。所以,原数组的元素顺序可以改变。
但是,在如何排序的时候我没有想明白。我不知道该怎样的对原始序列进行排序。因为,既然不是非黑即白,那你排序的依据是什么呢?
答案的方法很秒,是按第一个元素先来从小到大排序。注意,第一个元素是较短的那一条边。然后,在这个基础上,问题就转化为在第二条边上的lis问题。这点是非常秒的,非常好的办法。并且,对于第二条边判断的时候,其实 也还是要考虑第一条边,因为要避免相等的情形。也就说它把相等的情形放在这里处理了。这是非常秒的办法。对于前面的排序,如果你处理了相等的情形,那么没法排序了。总之,非常好的办法。
#include
#include
#include
#include
#define N 1000
//#define LOCAL
struct element{
int a_;
int b_;
element(){std::memset(this, 0, sizeof(element));}
element(int a, int b) : a_(a), b_(b) {}
bool operator<( const element& rhs ) const {
if( this->a_ != rhs.a_ ) return this->a_ < rhs.a_;
else return this->b_ < rhs.b_;
}
};
element arr[N];
int dp[N]; // dp[i]表示以i结束的最长路
int cal_lis( int n );
bool exist_edge( const element& lhs, const element& rhs );
int main( void ){
#ifdef LOCAL
std::ifstream cin("input.dat");
#endif
int t = 0;
std::cin >> t;
while(t--){
int n = 0;
std::cin >> n;
for( int i = 0; i < n; ++i ){
int a, b;
std::cin >> a >> b;
arr[i].a_ = std::min(a,b);
arr[i].b_ = std::max(a,b);
}
std::sort( arr, arr+n );
int ans = cal_lis(n);
std::cout << ans << std::endl;
}
#ifdef LOCAL
cin.close();
#endif
return 0;
}
int cal_lis( int n ){
dp[0] = 1;
int max = dp[0];
for( int i = 1; i < n; ++i ){
dp[i] = 1;
for( int j = 0; j < i; ++j ){
if( exist_edge( arr[j], arr[i] ) )
dp[i] = std::max( dp[i], dp[j] + 1 );
}
max = std::max(max, dp[i]);
}
return max;
}
bool exist_edge( const element& lhs, const element& rhs ){
return ( lhs.a_ < rhs.a_ && lhs.b_ < rhs.b_ )||( lhs.a_ < rhs.b_ && lhs.b_ < rhs.a_ );
}