首先让我们来复习一下最大上升子序列
portal : Longest Ordered Subsequence
首先想暴力做法,就是每次遍历到一个元素,要找以当前元素为结尾的最长上升序列,就是要在前面元素中找到一个结尾元素小于当前元素,并且最大长度的那个。如果用f[i]表示前i个数的最长长度
for(int i=0;i<n;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(x[j]<x[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
m = max(m,dp[i]);
}
printf("%d",m);
稍微想一下就知道这个复杂度不得行。但是如果要记录上升轨迹就必须严格dp,这里只用求LIS长度,所以不需要严格dp。
那么怎么办呢?
首先把数列的第一个元素丢到dp数组(存放上升子序列)中,找个index指向dp数组中的最后一个元素,如果数列后面有元素>index所指元素,则扩充dp数组;否则就在dp数组中二分查找严格小于该数的第一个数,用本数替换掉本来在那个小数的正后方的那个数。
#include<iostream>
#include<vector>
using namespace std;
int n;
int main(int argc, char const *argv[])
{
cin >> n;
vector<int> dp(n, 0); vector<int> s(n, 0);
for (int k = 0; k < n; ++k) cin >> s[k];
int i = 0, j = s[0];//i就是那个index
dp[i] = j;
for (int k = 1; k < n; ++k){
if(s[k] > dp[i]) dp[++i] = s[k];//严格大于dp的最后一个元素就扩充dp
else{
int l = 0, r = i, mid;
while(l < r){
mid = (l + r) >> 1;
if(dp[mid] > s[k]) r = mid;
else l = mid + 1;
}
dp[l] = s[k];//找到上图中9的位置,把3替换过去
}
}
cout << i + 1 << endl;//i是从0开始的
return 0;
}
注意:
①每次替换的一定是dp数组中比该数小的最大数的右边的数。
②此法dp记录的递增序列不一定是按照原序列顺序(详见紫色批注部分)
再来看一道裸的线性dp:买电脑
#include<iostream>
#define INF 1 << 29
using namespace std;
int n, c;
int f[10005], m[10005][10005];
int main(int argc, char const *argv[])
{
while(~scanf("%d%d", &c, &n)){
f[0] = 0;
for(int i = 1; i <= n; i++) f[i] = INF;
//要求最小,则初始化为最大
for (int i = 1; i <= n; ++i){
for (int j = i; j <= n; j++) scanf("%d", &m[i][j]);//m[i][j]表示第i年到第j年的维护费用
}
for (int i = 1; i <= n ; ++i){
for (int j = 1; j <= i; j++){
f[i] = min(f[i], f[j - 1] + m[j][i] + c);
//如果要买电脑的花费=前(j-1)年费用+第j年到第i年的维护费用+买电脑的费用
}
}
printf("%d\n", f[n]);
}
return 0;
}
足够裸,裸到O(n^2)都过了。。
其实这题给我最大的启发就是当下最优解=前面子问题的最优解+转移代价。
然后再来看wx小哥哥拉的dp专练~
多段连续字段和最大值
portal : max sum plus plus
我们来回忆一下之前最大子段和我们是怎么解决的?是不是每次要么附加在我们的已选数组最后,要么另起炉灶,然后每次滚一遍最大和。
那么我们多段怎么处理呢?是不是要么附加在最后一段后面(与最后一段合并),要么另起一段?
这题我感觉是属于那种说起来简单然后代码有点无从下手的。
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
#define INF 1 << 29//定义无穷大的好方法
const int N = 1000005;
int d[N], pre[N], num[N];
int m, n;
int main(int argc, char const *argv[]){
while(~scanf("%d%d", &m, &n)){
memset(d+1, 0, n * 4);//从第一个开始memset,注意第三个参数是字节数
memset(pre+1, 0, n * 4);
int tmp;
for(int i = 1; i <= n; i++) scanf("%d", &num[i]);
for(int i = 1; i <= m; i++){
tmp = -INF;
for(int j = i; j <= n; j++){
d[j] = max(d[j-1], pre[j-1]) + num[j];
pre[j-1] = tmp;//注意这里更新的是pre[j-1]
tmp = max(tmp, d[j]);
}
}
printf("%d\n", tmp);
}
return 0;
}
i=1时,pre记录的是一段的最大和;
i=2时,pre被两段最大和更新,但是d依旧用之前的pre计算(因为目前算两段,新起了一段,前面只需要一段)
以此类推…
1)解锁memset新用法:从想开始的位置开始,初始化想要的个数,同时注意只能全赋为0或-1,全赋1会出问题。因为memset是一个字节一个字节设置的,取要赋的值的后8位二进制进行赋值。1的二进制是(00000000 00000000 00000000 00000001),取后8位(00000001),int型占4个字节,当初始化为1时,它把一个int的每个字节都设置为1,也就是0x01010101,二进制是00000001 00000001 00000001 00000001,十进制就是16843009。所以第三个参数要填字节数。
2)若每次更新只涉及 i 和 i - 1 之间的取舍,那么完全可以利用滚动数组省去一维空间。
3)定义无穷大可用#define INF 1 << 29
又是一道裸题。。吗?
Portal : Ignatius and the Princess IV
芜湖,先暴力然后贡献了一发TLE~那好吧,再看看题。
en 结构体数组是个好东西,而且貌似int的范围在1e6之内,可以用数组下标存数,数组值存出现次数。
#include<iostream>
#include<string.h>
using namespace std;
const int N = 1e6 + 10;
int book[N], arr[N];//book[x]记录数x的位置
struct node{
int x, ans;
}dp[N];//记录数x和出现次数
int n;
int main(int argc, char const *argv[]){
while(~scanf("%d", &n)){
int m = (1 + n) / 2;
memset(book, -1, sizeof book);
memset(dp, 0, sizeof dp);
int cnt = 0;
for(int i = 0; i < n; i++) scanf("%d", &arr[i]);
for(int i = 0; i < n; i++){
int a = arr[i];
int index = book[a];
if(index == -1){//如果没出现过
book[a] = cnt;
dp[cnt].x = a;
dp[cnt++].ans++;
}else{//如果出现过
dp[index].ans++;
}
if(dp[index].ans >= m){
printf("%d\n", dp[index].x);
break;
}
}
}
return 0;
}
其实这一题有一种既神奇又简单的解法——我愿称之为“一瞄法”,你看它给的数字个数为奇,所以(1+n)/2必为偶数,排序之后,无论那个出现次数>=(1+n)/2的数在哪里,它总会经过(1+n)/2的下标,所以就捉住你啦~
其实多审审题,多动动脑,可以省很多代码,少费许多功夫呢!这种简单方法多省事!
再来一题!
Portal : Monkey and Banana
这题最奇怪一点是自己可以转一下方向然后堆在自己上面,而且数量不限,但是仔细一想你就会发现,自己可以堆在自己上面的情况屈指可数。仔细读题会发现一次堆箱子的个数不会超过30,为什么呢?我们是不是可以把转一下之后的情况算作不同长宽高的箱子,反正空间多的是,而且绝对够。
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int dp[110];
struct node{
int l, w, h;//length,width,height
}box[110];
bool cmp(node a, node b){
if(a.l != b.l) return a.l > b.l;//长更长的放前面
else return a.w > b.w;//宽更宽的放前面
}
int n;
int main(int argc, char const *argv[]){
int a, b, c;
int g = 0;
while(cin >> n, n){//当n为0时不再读入
int k = 0;
memset(dp, 0, sizeof dp);
memset(box, 0, sizeof box);
for(int i = 1; i <= n; i++){
cin >> a >> b >> c;
box[++k].l = a; box[k].w = b; box[k].h = c;//按高分类,三种,要么c
if(box[k].l < box[k].w) swap(box[k].l, box[k].w);//保证箱子的长>宽
box[++k].l = b; box[k].w = c; box[k].h = a;//按高分类,三种,要么a
if(box[k].l < box[k].w) swap(box[k].l, box[k].w);//保证箱子的长>宽
box[++k].l = c; box[k].w = a; box[k].h = b;//按高分类,三种,要么b
if(box[k].l < box[k].w) swap(box[k].l, box[k].w);//保证箱子的长>宽
}
box[0].l = 1e9, box[0].w = 1e9, box[0].h = 0;//第一个长宽无限,高为0
sort(box+1, box+k+1, cmp);
int maxx = 0;
for(int i = 1; i <= k; i++){
for(int j = 0; j < i; j++){
if(box[i].l < box[j].l && box[i].w < box[j].w){//若是后面箱子长宽严格小于前面箱子则可摞
dp[i] = max(dp[i], dp[j] +box[i].h);
maxx = max(maxx, dp[i]);
}
}
}
g++;
cout << "Case " << g << ": maximum height = " << maxx << endl;
}
return 0;
}
其实这题也很暴力,主要是要想到怎么排序,怎么处理这些箱子。
其实这篇blog10月中旬就动笔了,到现在才写完,我真的要好好反省下了┭┮﹏┭┮