动态规划 总结

一、斐波那契 跳台阶

能写出迭代就别用递归~


二、错排公式

小明给n(1letter[1] = 0;
letter[2] = 1;
【下标表示人数,0不用】


n >= 3时,将信封按顺序从1到n编号。在任意一种错装方案中,假设n号信封里装的是k号信封的信,而n号信封的信装在m号信封里。
1、若k == m。把n和m号信封里的信进行交换后,两者就是正确的,即除他们之外的剩余的n-2个信封全部错装,就是F[n-2];而m有n-1种可能取值(1到n-1任取一个),所以此类方式的可能性是(n-1)*F[n-2].
也可以理解为,在前n-2个错装的F[n-2]种方式的基础上,交换最后两个信封中的信,n信封和另外n-1中的任意一个信封进行交换(n-1种选择)。
2、若k != m。把n和m号信封里的信进行交换后,n号信封装的就是正确的,而m信封里装的是k信封的信,即除n之外的剩余的n-1个信封全部错装,就是F[n-1];而m有n-1种可能取值(1到n-1任取一个),所以此类方式的可能性是(n-1)*F[n-1].
也可以理解为,在有n-1个错装的F[n-1]种方式的基础上,将正确的n与另外n-1中的任意一个进行交换(n-1种选择),就得到所有信封全部装错的方式数。
综上所述,著名的错排公式:F[n] = (n-1) * F[n-1] + (n-1) * F[n-2]


三、最长递增子序列【时间复杂度O(n²),空间复杂度O(n)】

在一个已知序列[a1, a2, ,,, , an]中,取出若干个数,组成[ai1,ai2, ,,, , aim],其中下标i1, i2, im保持递增。

用F[i]表示递增子序列以ai结束时的最长长度,F[1] = 1.

推导F[i]:

假设F[1]~F[i-1]的值都已求出,以ai结尾的递增子序列,除了长度为1的情况,ai都是紧跟着一个由aj(j < i)组成的递增子序列之后。欲求以ai结尾的最长递增子序列长度F[i],需依次比较ai之前所有的aj(j < i)。若aj  < ai,则说明ai可以跟在以aj结尾的递增子序列之后,形成一个新的递增子序列,长度为F[j] + 1。取所有这些长度的最大值,得到F[i]。当没有一个ai,那么长度为1。

所以F[i] = max { 1, F[j]+ 1 | 所有满足ai且j < i的j }

代码:

#include
int arr[21];//存输入的数组
int dp[21];//记录各位置结尾的最长递增子序列长度
int max (int a, int b) {
    return a > b? a: b;
}
int main () {
	int n;//数据输入长度
	while (scanf("%d", &n) != EOF) {
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &arr[i]);
		}

		//求出各位置结尾的最长递增子序列长度
		for (int i = 1; i <= n; ++i) {
			int tmp = 1;//长度至少为1
			for (int j = 1; j < i; ++j){
				if (arr[i] > arr[j])
					tmp = max(tmp, dp[j] + 1);
			}
			dp[i] = tmp;//此时的tmp是该位所有结果中的最长的
		}

		//找出dp中的最大值
		int ans = 1;//至少为1
		for (int i = 1; i <= n; ++i) {
			ans = max(ans, dp[i]);
		}
		printf("%d", ans);
	}
	return 0;
}

合唱队形问题(正反两次求最长递增子序列):

N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,
则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入:
输入的第一行是一个整数N(2 <= N <= 100),表示同学的总数。
第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。

代码:

#include
int student[21];//存输入的数组
int dpLeft[21];//记录各位置结尾的最长子序列长度
int dpRight[21];
int max (int a, int b) {
    return a > b? a: b;
}
int main () {
	int n;//数据输入长度
	while (scanf("%d", &n) != EOF) {
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &student[i]);
		}

		//从左开始求的最长递增子序列长度
		for (int i = 1; i <= n; ++i) {
			int tmp = 1;//长度至少为1
			for (int j = 1; j < i; ++j){
				if (student[i] > student[j])
					tmp = max(tmp, dpLeft[j] + 1);
			}
			dpLeft[i] = tmp;//此时的tmp是该位所有结果中的最长的
		}

		//从右开始求的最长递增子序列长度
		for (int i = n; i >= 1; --i) {
			int tmp = 1;//长度至少为1
			for (int j = n; j > i; --j){
				if (student[i] > student[j])
					tmp = max(tmp, dpRight[j] + 1);
			}
			dpRight[i] = tmp;//此时的tmp是该位所有结果中的最长的
		}


		//找出最大值
		int ans = 1;//至少为1
		for (int i = 1; i <= n; ++i) {
			ans = max(ans, dpLeft[i] + dpRight[i] - 1);
		}
		printf("%d", n - ans);
	}
	return 0;
}

最长递增子序列O(nlogn)算法:  

用到了二分查找,所以查找从n降到了logn。

此二分查找即为【二分查找】里的low_bound版的插入版~ 若相同就更出现的第一个,若无结果,就在比它大的第一个位置更新~!

状态转移方程:dp[i]  =  max{dp[i], dp[j] + 1}, 1 <= j < i,data[j] < data[i].  
分析:加入下标x>y,dp[x] <= dp[y], 则x相对于y更有潜力。应替换  
首先根据dp[]值分类,记录满足dp[t] = k的最小的值data[t],记record[k] = min{data[t]},dp[t]=k.  
    1.发现record[k]在计算过程中单调不上升  
    2.record[1] 解法:  
1. 设当前最长递增子序列为len,考虑元素data[i];  
2. 若record[len]    否则,在record[0]~record[len]中二分查找,找到第一个比它小的元素record[k],并record[k+1]=data[i](递增子序列 更新)

#include 
#include 
#include 
using namespace std;
const int N = 41000;
int data[N];       //data[i]:原始数据
int record[N];       //record[i]:长度为i的递增子序列的尾部值

int BinSearch_LowBound(int key, int* record, int low, int high)
{
    while(low != high)
    {
        int mid = (low + high) >> 1;
        if(record[mid] < key)
            low = mid + 1;
        else
            high = mid;
    }
    return low;
}

int LIS(int* data, int n, int* record)  //数组首元素从1下标开始
{
    int i,j;
    record[1] = data[1];
    int len = 1;        //递增子序列长度
    for(i = 2; i <= n; i++)
    {
        if(record[len] < data[i])    //递增子序列末尾填一个 所以len也加一
            j = ++len;
        else    //不填 看这个data[i] 能在已知的多个递增子序列中 搁哪里
            j = BinSearch_LowBound(data[i], record, 1, len) + 1;
        record[j] = data[i];
    }
    return len;
}

int main()
{
    int time;
    int len;
    scanf("%d", &time);
    while(time--)
    {
        scanf("%d", &len);
        for(int i = 1; i <= len; i++)
            scanf("%d", &data[i]);
        printf("%d\n", LIS(data, len, record));
    }
    return 0;
}

C++写法:

(searchInsert 1和2 都对。其功能:target有多个时,返回下标最小的;target有一个时,返回该下标;没有target时,返回应插入位置的下标)

35. Search Insert Position 和 300. Longest Increasing Subsequence

class Solution {
public:
    int searchInsert1(vector record, int target) {
        int low = 0, high = record.size()-1;
        while (low <= high) {
            int mid = low + (high - low) >> 1;
            if (record[mid] < target)
                low = mid + 1;
            else
                high = mid - 1;
        }
        return low;
    }
    int searchInsert2(vector record, int target){
        int low = 0, high = record.size() - 1;
        while(low != high){
            int mid = low + (high - low) >> 1;
            if(record[mid] < target)
                low = mid + 1;
            else
                high = mid;
        }
        return low;
    }
    
    int lengthOfLIS(vector& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        
        vector dp;
        dp.push_back(nums[0]);
        
        for(int i = 1; i < n; ++i) {
            if(nums[i] > dp.back()) {
                dp.push_back(nums[i]);
            }
            else {
                int insertIndex = searchInsert1(dp, nums[i]);
                dp[insertIndex] = nums[i];
            }
        }
        return dp.size();
    }
};

int lengthOfLIS(vector& nums) {
    vector dp;
    for(int i = 0; i < nums.size(); i++) {
        auto it = std::lower_bound(dp.begin(), dp.end(), nums[i]);
        if(it == dp.end()) dp.push_back(nums[i]);
        else *it = nums[i];
    }
    return dp.size();
}


四、最长公共子序列【时空复杂度皆为O(len1 * len2)】

递推式:

假设有两个字符串str1和str2,长度分别是len1,len2。用dp[i][j]表示str1前i个字符组成的前缀子串与str2前j个字符组成的前缀子串的最长公共子串长度,那么:
dp[i][0] = 0, 0 ≤ i ≤ len1.
dp[0][j] = 0, 0 ≤ j ≤ len2.
dp[i][j] = dp[i-1][j-1] + 1, str1[i] == str2[j]
dp[i][j] = max{dp[i-1][j], dp[i][j-1]}, str1[i] != str2[j]
最终dp[len1][len2]即为所求~


解析:

用dp[i][j]表示str1中前i个字符和str2中前j个字符分别组成的两个前缀字符串的最长公共子串长度。

显然,dp[i][0] = 0, dp[0][j] = 0. 

假设我们已经求得dp[i][j](0 <= i < x, 0 <= j < y),接下来考虑如何推得dp[x][y].
1、若str1[x] == str2[y],则他们都是各自前缀字符串的最后一个字符,所以必存在一个最长公共子串以str1[x]或str2[y]结尾,其他部分等价于str1中前x-1 个字符和str2中前y-1 个字符的最长的公共子串。所以这个子串的长度比dp[x-1][y-1]增加1,即dp[x][y] = dp[x-1][y-1] + 1.
2、若str1[x] != str2[y],此时最长公共子串长度为“str1前x-1 个字符和str2中前y个字符的最长公共子串长度”与“str1前x个字符和str2中前y-1 个字符的最长公共子串长度” 两者的较大者。即在两种情况下得到的最长公共子串都不会因为其中一个字符串又增加了一个字符长度而发生改变(因为加的是不相同的!公共长度怎么可能增加呢),即dp[x][y] = max{dp[x-1][y], dp[x][y-1]}.

代码:

#include
#include
int dp[101][101];

int max (int a, int b) {
    return a > b? a: b;
}
int main () {
    char str1[101];
    char str2[101];
    while (scanf("%s%s", &str1, &str2) != EOF) {
        memset(dp, 0, sizeof(dp));
        int len1 = strlen(str1);
        int len2 = strlen(str2);
        for(int i = 1; i <= len1; ++i) {
            for(int j = 1; j <= len2; ++j) {
                if (str1[i] == str2[j])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
        printf("%d",dp[len1][len2]);
    }
    return 0;
}



六、背包问题

背包问题【01、完全(恰好or不超过)、多重】【尚未整理完】

你可能感兴趣的:(总结,【笔面试准备】)