#2020寒假集训#动态规划DP入门(Dynamic Programming 算法思想)代码笔记

前言

  • 对于DP算法,比如今天所介绍的LCS和LIS问题
  • 有一组很重要的思想就是——递归变递推,空间换时间
  • 也就是说,原本LCS和LIS靠递归可以完成
  • 但每一次询问一次结果就要从头到尾递归
  • 当进行多次询问的时候,就多浪费了很多时间,做了很多重复的事情
  • 所以为了免去这些重复的事情,我们就把函数的递归变成数组的递推
  • 用数组记录当前结果,下一层结果若用到之前的数据直接调用即可
  • 那么DP问题的重难点就在于如何找到这些dp数组的关系,列出转移方程

易混淆

  • 子序列不一定连续
  • 子串一定要连续
  • 共同不可颠倒原字符出现的顺序

LCS 最长公共子序问题

简述

  • 假设待求解的两个序列为Xn和Ym,我们用L{Xn,Ym}表示最长公共子序
  • 如果我们已经知道了L{Xn-1,Ym-1}的最长公共子序
  • L{Xn,Ym}=L{Xn-1,Ym-1}+1 或者 L{Xn,Yn}=max{L{Xn-1,Ym},L{Xn,Ym-1}}
  • 上述两种方程的区别就在于这一组坐标对应的的x和y的值是否一样
  • 注意dp数组下标代表第几个字符的话,输入的x和y字符串下标从0开始!!!
  • 如果是两个字符串解LCS问题,就开二维dp数组;如果三个,就开三维……

转移方程

在这里插入图片描述
Common SubsequenceHZNU19training题源

Background
A subsequence of a given sequence is the given sequence with some elements (possible none) left out. Given a sequence X = <x1, x2, …, xm> another sequence Z = <z1, z2, …, zk> is a subsequence of X if there exists a strictly increasing sequence <i1, i2, …, ik> of indices of X such that for all j = 1,2,…,k, xij = zj. For example, Z = <a, b, f, c> is a subsequence of X = <a, b, c, f, b, c> with index sequence <1, 2, 4, 6>. Given two sequences X and Y the problem is to find the length of the maximum-length common subsequence of X and Y.
The program input is from a text file. Each data set in the file contains two strings representing the given sequences. The sequences are separated by any number of white spaces. The input data are correct. For each set of data the program prints on the standard output the length of the maximum-length common subsequence from the beginning of a separate line.

Input
abcfbc abfcab
programming contest
abcd mnp

Output
4
2
0

Sample Input
abcfbc abfcab
programming contest
abcd mnp

Sample Output
4
2
0

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<string>
#include<algorithm>
using namespace std; 
int dp[1010][1010];
//二维数组占用较大内存时, 记得开在全局变量 
char a[1010],b[1010];
int main()
{
	while(~scanf("%s %s",&a,&b))
	{
		memset(dp,0,sizeof dp);//sizeof(dp)和sizeof dp均可 
		int Max=0;
		for(int i=1;i<=strlen(a);++i)
		{
			for(int j=1;j<=strlen(b);++j) 
			{
				if(a[i-1]==b[j-1])//i-1表示的是字符串下标(从0开始的) 
					dp[i][j]=dp[i-1][j-1]+1;//dp[i][j]的i和j是a、b字符串第几个字符的对应 
				else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
				if(dp[i][j]>Max) Max=dp[i][j];
			}
		}
		printf("%d\n",Max);
	}
}

LIS最大上升子序列问题

简述

  • 对于最大上升子序列可以参考最大公共子序列的解法
  • 只需要再开一个数组保存原数组并进行sort升序排序
  • 然后对这两个数组进行LCS问题解法即可
  • 但是!!!这类问题如果它原数据就给了1e6
  • 然后按照LCS的解法就要开一个二维的dp数组,二维的1e6就有了1e12
  • 普通计算机根本没法通过!!!
  • 所以这类题目得开一维的dp数组进行记录
  • dp[i]=max(dp[i]),dp[j]+1),i>j && num[i]>num[j]
  • 也就是做双重循环,对于dp[i],要遍历 i 之前的所有 j 如果输入的值是 i 的大于 j 的就可以长度+1
  • 但最长的到底是 j 层循环里的哪一层呢,就靠max函数啦
  • 每次记录当前最长,然后下一次遇到dp[j]可以+1的时候,看看是原来的dp[i]大,还是现在+1的大

转移方程

一般…按“简述”里的方程写叭 ❥(ゝω・✿ฺ)
在这里插入图片描述
Mysterious Present HZNU19training题源

Background
Peter decided to wish happy birthday to his friend from Australia and send him a card. To make his present more mysterious, he decided to make a chain. Chain here is such a sequence of envelopes A = {a1,  a2,  …,  an}, where the width and the height of the i-th envelope is strictly higher than the width and the height of the (i  -  1)-th envelope respectively. Chain size is the number of envelopes in the chain.
Peter wants to make the chain of the maximum size from the envelopes he has, the chain should be such, that he’ll be able to put a card into it. The card fits into the chain if its width and height is lower than the width and the height of the smallest envelope in the chain respectively. It’s forbidden to turn the card and the envelopes.
Peter has very many envelopes and very little time, this hard task is entrusted to you.

Input
The first line contains integers n, w, h (1  ≤ n ≤ 5000, 1 ≤ w,  h  ≤ 106) — amount of envelopes Peter has, the card width and height respectively. Then there follow n lines, each of them contains two integer numbers wi and hi — width and height of the i-th envelope (1 ≤ wi,  hi ≤ 106).

Output
In the first line print the maximum chain size. In the second line print the numbers of the envelopes (separated by space), forming the required chain, starting with the number of the smallest envelope. Remember, please, that the card should fit into the smallest envelope. If the chain of maximum size is not unique, print any of the answers.
If the card does not fit into any of the envelopes, print number 0 in the single line.

Examples
Input
2 1 1
2 2
2 2
Output
1
1
Input
3 3 3
5 4
12 11
9 8
Output
3
1 3 2

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<stack>
#include<cstdlib>
#include<queue>
#include<set>
#include<string.h>
#include<vector>
#include<deque>
#include<map>
using namespace std;

#define INF 0x3f3f3f3f3f3f3f3f//long long 无穷大
#define inf 0x3f3f3f3f//int 无穷大
#define eps 1e-4
#define bug printf("*********\n")
#define debug(x) cout<<#x"=["<<x<<"]" <<endl
#define ll long long
#define LL long long
const int MAXN=1e5+10;
const int mod=998244353;

struct node
{
    int w,h;
    int id;//id储存这对数据是第几次输入的 
}a[MAXN];

bool cmp(node a,node b)
{//宽度小的在前,宽度一样小的高度小的在前 
    if(a.w!=b.w) return a.w<b.w;
    return a.h<b.h;
}

int dp[MAXN];//记录从第i个信封开始最多能套多少个信封 
int path[MAXN];//记录从第i个信封套哪个信封最优 

void print(int n)
{
    if(path[n]==-1)//==-1就没有下一个信封了,输出最后一个后,return结束
	{
        printf("%d ",a[n].id); 
        return;
    }
    /*
		i的信封大,j的信封小,path记录的是i直接套着的j
		但题目要求是信封从小到大输出,所以最后一个j要被先输出
		传入的n的id等前面递归完毕最后再输出 
	*/
	else 
	{
        print(path[n]);
        printf("%d ",a[n].id);
		//鼠标划一下样例,可以发现最后一个数据后面也有空格 
    }
}

int main()
{
    int n,w,h;
    cin>>n>>w>>h;//w和h是卡片的宽和高 
    int pos=0;
    for(int i=1;i<=n;i++)
	{
        int w1,h1;
        cin>>w1>>h1;//按次序循环输入信封的宽和高 
        if(w1>w&&h1>h) 
		{//仅当能装得下卡片的时候,才保留信封,存值进结构体数组a
            a[pos].w=w1;
            a[pos].h=h1;
            a[pos].id=i;
			//最后一行输出的就是这个id,记录的是它第几个被输入 
            pos++;
        }
    }
    if(pos==0)//所有信封都装不下卡片,则没有数据进数组a,pos不涨仍为0 
    {
        cout<<0<<endl;
        return 0;
    }
    sort(a,a+pos,cmp);//对装得下卡片的信封进行宽度优先其次高度的升序排序 
    for(int i=0;i<pos;i++)
	{
        dp[i]=1;//初始化自己只套了自己这一个信封 
        path[i]=i;//初始化自己走向自己(自己套的是自己) 
    }
    memset(path,-1,sizeof path);
    int maxlen=1,biao=0;
	//maxlen记录最多能套几个信封 
    for(int i=0;i<pos;i++)//第i个是最外面的信封!!! 
	{
        for(int j=0;j<i;j++)
		{
            if(a[j].h<a[i].h&&a[j].w<a[i].w&&dp[i]<(dp[j]+1))
			{
			/*
				遍历 j 满足条件如下: 
				条件1.高度比i的小;
				条件2.宽度比i的小;
				条件3.如果dp[j]中的j之前被当做i的时候已经得到过dp
				那么这次的dp[i]它可能下一层套前面的j能套更多
				就没必要下一层套这个j,所以若要套(+1算是套上) 
				就得保证套上后,比现在已有的dp[i]要大,不然就退化啦 
			*/ 
                dp[i]=dp[j]+1;
                if(dp[i]>maxlen) maxlen=dp[i];//每次获取dp[i]顺便记下最大值 
                path[i]=j;//记录这次的路径是从i走到哪个j 
            }
        }
    }
    int tmp,cmp=0;
    for(int i=0;i<n;i++)
	{
        if(dp[i]>cmp)//寻找从哪个i开始套能被套最多的信封
		{
            cmp=dp[i];//相当于是信封数量的最值 
            tmp=i;//记录从里到外套信封的其实位置 
        }
    }
    cout<<maxlen<<endl;
    print(tmp);
    printf("\n"); 
}

LIS最大上升子序列问题(NlogN模板套用)

  • 对于基础模板因为是双重循环遍历,所以时间复杂度是 n2

  • 但是如果题目给的数据很大,时间又很短,就很有可能TLE啦

  • 所以对LIS问题的模板是有很多种优化方法哒!比如优化到 nlogn 的时间复杂度~

  • 今天,我们就先来学习用low数组lower_bound函数进行二分查找的优化吧(o゚▽゚)o ノ"

  • 优化想法是,一个上升子序列,结尾元素越小,越有利于在后面接其他元素

  • 所以对于一个新元素,它如果比前面的上升序列尾元素大的话,就接到末尾

  • 但如果比前面的尾元素小的话,就替换(更新)掉从小到大第一个大于等于它的元素

  • 这个元素可以二分查找,也可以用lower_bound函数直接第一个大于等于它的元素的地址

  • 这个方法记录的上升序列不一定是子序列 而是 长度为最长上升子序列的 上升序列

所以优化后得到的 【长度】【尾元素】 与实际最长上升子序列相同!!!

下面我们来康一道例题感受一波~
Bridging signals HZNU19training题源

Background
‘Oh no, they’ve done it again’, cries the chief designer at the Waferland chip factory. Once more the routing designers have screwed up completely, making the signals on the chip connecting the ports of two functional blocks cross each other all over the place. At this late stage of the process, it is too
expensive to redo the routing. Instead, the engineers have to bridge the signals, using the third dimension, so that no two signals cross. However, bridging is a complicated operation, and thus it is desirable to bridge as few signals as possible. The call for a computer program that finds the maximum number of signals which may be connected on the silicon surface without rossing each other, is imminent. Bearing in mind that there may be housands of signal ports at the boundary of a functional block, the problem asks quite a lot of the programmer. Are you up to the task?
在这里插入图片描述
Figure 1. To the left: The two blocks’ ports and their signal mapping (4,2,6,3,1,5). To the right: At most three signals may be routed on the silicon surface without crossing each other. The dashed signals must be bridged.
A typical situation is schematically depicted in figure 1. The ports of the two functional blocks are numbered from 1 to p, from top to bottom. The signal mapping is described by a permutation of the numbers 1 to p in the form of a list of p unique numbers in the range 1 to p, in which the i:th number pecifies which port on the right side should be connected to the i:th port on the left side.
Two signals cross if and only if the straight lines connecting the two ports of each pair do.

Input
On the first line of the input, there is a single positive integer n, telling the number of test scenarios to follow. Each test scenario begins with a line containing a single positive integer p<40000, the number of ports on the two functional blocks. Then follow p lines, describing the signal mapping: On the i:th line is the port number of the block on the right side which should be connected to the i:th port of the block on the left side.

Output
For each test scenario, output one line containing the maximum number of signals which may be routed on the silicon surface without crossing each other.

Sample Input
4
6
4
2
6
3
1
5
10
2
3
4
5
6
7
8
9
10
1
8
8
7
6
5
4
3
2
1
9
5
8
9
2
3
1
7
4
6

Sample Output
3
9
1
4

#include<stdio.h>  
#include<string.h>  
#include<algorithm>  
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=4e4+10;
int g[maxn],d[maxn],num[maxn];  
int main()  
{
	int N;
	scanf("%d",&N);
	while(N--)
	{
		int p;
		scanf("%d",&p);
		fill(g,g+p,INF);
		int Max=-1;
		for(int i=0;i<p;i++)
		{
			scanf("%d",&num[i]);
		}
    	for(int i=0;i<p;i++)  
    	{
        	int j=lower_bound(g,g+p,num[i])-g;  
        	d[i]=j+1;  
        	if(Max<d[i]) Max=d[i];  
        	g[j]=num[i];
    	}  
    	printf("%d\n",Max);  
	}
    return 0;  
}

纯思维列转移方程的DP问题

虽然除了LCS和LIS问题还有很多很多的经典DP题型
但是也有很多是用到DP思想,却不归属于哪类的题型
重点就在于——分清情况列转移方程!!!比如下面这道题~

Hard problem HZNU19training题源

Background
Vasiliy is fond of solving different tasks. Today he found one he wasn’t able to solve himself, so he asks you to help.
Vasiliy is given n strings consisting of lowercase English letters. He wants them to be sorted in lexicographical order (as in the dictionary), but he is not allowed to swap any of them. The only operation he is allowed to do is to reverse any of them (first character becomes last, second becomes one before last and so on).
To reverse the i-th string Vasiliy has to spent ci units of energy. He is interested in the minimum amount of energy he has to spent in order to have strings sorted in lexicographical order.
String A is lexicographically smaller than string B if it is shorter than B (|A| < |B|) and is its prefix, or if none of them is a prefix of the other and at the first position where they differ character in A is smaller than the character in B.
For the purpose of this problem, two equal strings nearby do not break the condition of sequence being sorted lexicographically.

Input
The first line of the input contains a single integer n (2 ≤ n ≤ 100 000) — the number of strings.
The second line contains n integers ci (0 ≤ ci ≤ 109), the i-th of them is equal to the amount of energy Vasiliy has to spent in order to reverse the i-th string.
Then follow n lines, each containing a string consisting of lowercase English letters. The total length of these strings doesn’t exceed 100 000.

Output
If it is impossible to reverse some of the strings such that they will be located in lexicographical order, print  - 1. Otherwise, print the minimum total amount of energy Vasiliy has to spent.

Examples
Input
2
1 2
ba
ac
Output
1
Input
3
1 3 1
aa
ba
ac
Output
1
Input
2
5 5
bbb
aaa
Output
-1
Input
2
3 3
aaa
aa
Output
-1

Note
In the second sample one has to reverse string 2 or string 3. To amount of energy required to reverse the string 3 is smaller.
In the third sample, both strings do not change after reverse and they go in the wrong order, so the answer is  - 1.
In the fourth sample, both strings consists of characters ‘a’ only, but in the sorted order string “aa” should go before string “aaa”, thus the answer is  - 1.

#include<stdio.h>
#include<string.h>
#include<string>
#include<algorithm>
#include<iostream>
using namespace std;
const long long INF=0x3f3f3f3f3f3f3f3f;
//注意long long的INF要开8个3f 
string put[100010],fput[100010];//put用于输入字符串,输入后同时用fput记录反转后的字符串 
long long Min,cost[100010];//Min用于最后比较输出,cost输入每个字符串反转的耗能 
long long fz[100010],bfz[100010];//fz和bfz分别记录当前字符串反转和不反转后总的耗能 
//一堆1e5以上的数组记得开成全局变量,免得程序难以运行 
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		scanf("%lld",&cost[i]);
	}
	for(int i=1;i<=n;++i)
	{
		cin>>put[i];
		fput[i]=put[i];//记录一下输入的字符串用于反转 
		reverse(fput[i].begin(),fput[i].end());//algorithm里的函数,反转字符串或者数组 
	}
	//第一个字符串前面没字符串,只要自己升序就能保证前1个升序,注意初始化 
	fz[1]=cost[1]; //反转则花费自己的耗能 
	bfz[1]=0;//不反转则自己耗能为 0 
	/*
		bfz[i]第i个字符串不反转且使得前i个串升序的最小耗能
		fz[i]第i个字符串反转且使得前i个串升序的最小耗能 
	*/
	for(int i=2;i<=n;++i)
	{
		fz[i]=INF,bfz[i]=INF;
		//如果i位置反转或者不反转都不能满足题意要求就是初始的INF无穷大 
		if(put[i]>=put[i-1]) bfz[i]=min(bfz[i],bfz[i-1]);
		if(put[i]>=fput[i-1]) bfz[i]=min(bfz[i],fz[i-1]);
		/*
			如果当前字符串不反转对于前一个字符串反转或不反转都能升序的话 
			两次if都经历了,就用到了min函数,要求更小的
			但如果只是和前一个字符串关系升序,再前面就不行了呢? 
			那么再前一个字符串被处理的时候,就没有找到能满足升序的情况
			bfz或fz的[i-1]就是INF,能表示不满足题意要求 
		*/
		if(fput[i]>=put[i-1]) fz[i]=min(fz[i],bfz[i-1]+cost[i]);
		if(fput[i]>=fput[i-1]) fz[i]=min(fz[i],fz[i-1]+cost[i]);
	}
	Min=min(bfz[n],fz[n]);
	if(Min==INF) printf("-1\n");
	else printf("%lld\n",Min);
}
/*
	题意要求:
	给定n个字符串,问要让这些字符串是从小到大字典序的关系需要多少耗能
	而让这些字符串从小到大字典序,只能通过反转当个字符串来实现,不能换位置
	字符串用string直接比较就行,反转用STL的reverse函数就行
	AB<CB;AA<AAA;AB<AC……也就是真实意义上的字典序 
	保证后一个字符串大于等于前一个字符串即可
	因为前一个字符串可能反转了可能没反转,后一个字符串也是可能反转可能不反转
	字符串反转前后比较对象也不一样,会有四种比较情况
	所以这四种比较情况分类写(四个if)
	自不反>=前不反;自不反>=前反;自反>=前不反;自反>=前反 
	写完之后保证最后一个也满足前i个字符串升序 
	记录到最后一个字符串的bfz和fz表示的耗能,出它们俩的Min就行 
*/

篇末小技巧

加速cin、cout的代码语句

有的时候需要输入string类型,必须用到cin、cout
注意不要scanf、cin、printf、cout混用!!!它会超级慢的!!!
不过cin和cout本身也很慢鸭,怎么办呢?
没关系,用下面的语句就能相对优化啦
虽然还是没有scanf和printf单独用来得快,但已经能快很多啦ヾ(◍°∇°◍)ノ゙

	iOS::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(false);
发布了22 篇原创文章 · 获赞 0 · 访问量 409

猜你喜欢

转载自blog.csdn.net/qq_46184427/article/details/104033805