动态规划
子序列问题
最长上升子序列
最长公共序列
以上两个的详解可以看这里
最长公共上升子序列
#include<bits/stdc++.h>
using namespace std;
const int N = 3010;
int a[N],b[N];
int f[N][N];
int res;
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++)
cin >> a[i];
for(int i = 1;i <= n;i ++)
cin >> b[i];
for(int i = 1;i <= n;i ++)
{
int max_fb = 1;
for(int j = 1;j <= n;j ++)
{
f[i][j] = f[i - 1][j];
if(a[i] == b[j])
f[i][j] = max(f[i][j],max_fb);
if(b[j] < a[i] && f[i - 1][j] + 1 > max_fb)
max_fb = f[i - 1][j] + 1;
}
}
for(int i = 1;i <= n;i ++) res = max(res,f[n][i]);
cout << res;
return 0;
}
背包问题
背包问题的二维优化
#include<bits/stdc++.h>
using namespace std;
const int N = 2020;
int n,m;
typedef pair<int ,int > PII;
vector<PII> good;
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1;i <= n;i ++)
{
int v,w,s;
cin >> v >> w >> s;
for(int j = 1;j <= s;j <<= 1)
{
s -= j;//将s进行二进制分解
good.push_back({v * j,w * j});//将分解后的作为一个新的物品
}
if(s) good.push_back({s * v,w * s});//不要忘了s可能剩下,其本身也应该作为一个物品
}
for(int i = 0;i < good.size();i ++)
for(int j = m;j >= good[i].first;j --)
f[j] = max(f[j],f[j - good[i].first] + good[i].second);
cout << f[m];
return 0;
}
背包的单调队列优化
#include<bits/stdc++.h>
using namespace std;
const int V = 20003;
int f[V];
int temp[V];
int q[V];
int n,m,v,w,s;
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++)
{
scanf("%d%d%d",&v,&w,&s);
memcpy(temp,f,sizeof(f));//temp用来储存上一个状态
for(int j = 0;j < v;j ++)//j枚举余数
{
int head = 0,tail = -1;
for(int k = j;k <= m;k += v)//k枚举每个%v = j的数
{
f[k] = temp[k];
if(head <= tail && (k - q[head]) / v > s) head ++;
if(head <= tail) f[k] = max(f[k],temp[q[head]] + (k - q[head]) / v * w);
while(head <= tail && temp[k] - (k - q[tail]) / v * w >= temp[q[tail]] )tail --;
q[++ tail] = k;
}
}
}
cout << f[m];
return 0;
}
简单的说一下思路:
每一次选择一个物品,我们都可以将0~m的体积变成许多个集合,集合的划分标准是以体积%物体体积划分
我们把余数相同的划分为同一个集合,不同集合之间在更新值的时候不会互相干扰
所以我们就将余数作为划分阶段
这里可以用单调队列优化,因为当枚举到体积k的时候,始终只能往前面取到k - s * c,所以用单调队列来维护这个区间的最大值
这里维护单调队列判断的时候,不能直接判断上一个状态下对应的值的大小,因为在后面的一般都比前面的值大
那么我们如何判断呢?(具体看代码)其实就是将当前将要加入的数对应的值减去 ( 当前的体积値 − t a i l ) / v ∗ w (当前的体积値 - tail) / v* w (当前的体积値−tail)/v∗w,如果减去之后剩下的值仍然大于g[tail]那么就说明tail对应的一定不是最优解
因为我们需要比较的其实是
假设
x
为将要加入的元素
我们就要比较
t
e
p
m
[
x
]
+
(
k
−
x
)
/
v
∗
w
和
t
e
p
m
[
t
a
i
l
]
+
(
k
−
t
a
i
l
)
/
v
∗
w
用前一个减去后一个得到
t
e
m
p
[
x
]
−
t
e
m
p
[
t
a
i
l
]
−
(
x
−
t
a
i
l
)
/
v
∗
w
再化简就变成了上面的式子
假设x为将要加入的元素\\ 我们就要比较tepm[x] +(k-x)/v * w和 tepm[tail] +(k-tail)/v * w\\ 用前一个减去后一个得到temp[x]-temp[tail] -(x - tail) / v * w\\ 再化简就变成了上面的式子
假设x为将要加入的元素我们就要比较tepm[x]+(k−x)/v∗w和tepm[tail]+(k−tail)/v∗w用前一个减去后一个得到temp[x]−temp[tail]−(x−tail)/v∗w再化简就变成了上面的式子
区间dp
区间问题的关键就在于,选择区间的长度作为状态,左右区间的位置l,r作为阶段,在l~r这个区间中的的划分点k作为决策
比较简单就不写
#include<bits/stdc++.h>
using namespace std;
const int N = 102;
const int MIN = -33786;
int f[N][N][2];//f[l][r][1]代表l~r这个区间内的最大值,f[l][r][0]代表l~r这个区间内的最小值
char s[N];//符号
int w[N];//数据
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++)
{
cin >> s[i] >> w[i];
s[i + n] = s[i];
w[i + n] = w[i];//将原来的复制一倍后接在后面(处理环形动态规划的常用方式)
}
for(int len = 1;len <= n;len ++)
{//枚举长度,长度最多为n
for(int l = 1;l <= n * 2 - len + 1;l ++)
{//枚举左边界
int r = l + len - 1;//右边界
if(len == 1) f[l][r][0] = f[l][r][1] = w[l];//如果长度为1,最大值和最小值都只能是它本身
else
{
f[l][r][0] = -MIN;
f[l][r][1] = MIN;//赋初始值
for(int k = l;k < r;k ++)
{//枚举分界点
if(s[k + 1] == 't')
{
f[l][r][1] = max(f[l][r][1],f[l][k][1] + f[k + 1][r][1]);
f[l][r][0] = min(f[l][r][0],f[l][k][0] + f[k + 1][r][0]);
}//如果是加号,最大值等于最大值相加最小值等于最小值相加
else
{
int a1 = f[l][k][0] * f[k + 1][r][1],
a2 = f[l][k][1] * f[k + 1][r][1],
a3 = f[k + 1][r][0] * f[l][k][1],
a4 = f[k + 1][r][0] * f[l][k][0];
f[l][r][1] = max(f[l][r][1],max(max(a1,a2),max(a3,a4)));
f[l][r][0] = min(f[l][r][0],min(min(a1,a2),min(a3,a4)));
}//如果是乘号,就无脑判断一遍四种情况的大小,因为可能存在最小值与最小值相乘比较大(当两者都为负数的时候)
}
}
}
}
int res = MIN;
for(int l = 1;l <= n;l ++) res = max(f[l][l + n - 1][1],res);
cout << res << endl;
for(int l = 1;l <= n;l ++)
if(f[l][l + n - 1][1] == res)
cout << l << " ";
return 0;
}
这道题,如果简单的划分按照上面的方式划分的话,就有可能产生重复的情况,所以这道题在划分的时候,必须还要加一点限制条件,就是枚举k时,k~r必须只有一棵子树,剩下部分可以随意.
以及,一棵树的序列,必须是奇数,所以在下面某些循环的时候需要+2,并且一棵树的序列的开头和结尾必然相同
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 303,mod = 1e9;
int f[N][N];
string s;
int n;
int solve(int l,int r)
{
if(s[l] != s[r]) return f[l][r] = 0;
if(l == r) return f[l][r] = 1;
if(f[l][r] != -1) return f[l][r];
f[l][r] = 0;
for(int k = l;k < r - 1;k += 2)
{
if(s[k + 1] == s[r - 1])
f[l][r] = (f[l][r] + (ll)solve(l,k) * solve(k + 1,r - 1)) % mod;
}
return f[l][r];
}
int main()
{
cin >> s;
n = s.size();
for(int i = 0;i <= n;i ++)
for(int j = 0;j <= n;j ++)
f[i][j] = -1;
if(n % 2 == 0)
{
puts("0");
return 0;
}
cout << solve(0,n - 1);
return 0;
}
状态压缩DP
状态压缩的典型模板题
首先进行分析,将每一行作为阶段,每一列的状态化为一个M位二进制数,我们用1代表这是一个还没有补充完整的竖着的长方条的上端,0代表其他情况
然后就有这样的性质:,从状态k转移到状态j,必须要满足,j和k按位与的结果必须是0,这样才能保证所有k状态中剩下半截的长方条能匹配成功.其次,j和k按位或的结果中,每一段连续的0的个数必须是偶数,这些0代表着若干个横着的长方形
然后就是可以在dp前预处理出 [ 0 , 2 M − 1 ] [0,2^M - 1] [0,2M−1]中所有满足"二进制下表示每一段连续的0都有偶数个"
怎样处理的细节见代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long f[12][1 << 11];
bool in_s[1 << 11];
int main()
{
while(cin >> n >> m)
{
for(int i = 0;i < 1 << m;i ++)//预处理数,用i来枚举
{
bool cnt = 0,has_odd = 0;
for(int j = 0;j < m;j ++)
if(i >> j & 1) has_odd |= cnt;//手动模拟一些数可知,如果一旦出现了连续的奇数个零,那么cnt会变成1,在下一次遇到1的时候,has_odd就会变成1.
else cnt ^= 1;
in_s[i] = has_odd | cnt ? 0 : 1;//最后还要再判断一次,因为最后一位不一定出现1,不会进入上面的那个条件语句
}//统计
f[0][0] = 1;//第0行没有伸出任何竖着的长方条,只有一种方案
for(int i = 1;i <= n;i ++)
for(int j = 0;j < 1 << m;j ++)
{
f[i][j] = 0;
for(int k = 0;k < 1 << m;k ++)
{
if((j & k) == 0 && in_s[j | k])
f[i][j] += f[i - 1][k];//累加
}
}
cout << f[n][0] << endl;
}
return 0;
}
用这个当例题写一下压缩dp的一些处理技巧
题目就不多说了。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 10;
ll f[N][100][82];
int n,m;
int state[100],cnt;
int c[100];
void lowbit(int x,int id)
{
while(x)
{
x -= (x & -x);
c[id] ++;
}//计算一个数里面有多少个1,用lowbit运算会快一点
return;
}
void address()
{
for(int i = 0;i < (1 << n);i ++)
{
if((i & (i << 1)) == 0)
state[++ cnt] = i;
}//判断左右两边是否有1的存在,将原来的数向左向右移动一位,如果与上原来的数之后的值不为0,就说明,有相邻的1;
}
int main()
{
cin >> n >> m;
address();
for(int i = 1;i <= cnt;i ++)
lowbit(state[i],i);
for(int i = 1;i <= cnt ;i ++) f[1][i][c[i]] = 1;
//初始化第一排
for(int i = 2;i <= n;i ++)
{
for(int j = 1;j <= cnt;j ++)
{
for(int k = 1;k <= cnt;k ++)
{
if((state[k] & (state[j] << 1)) == 0 && (state[k] & (state[j] >> 1)) == 0 && (state[k] & state[j]) == 0 )//判断是否合法
{
for(int x = c[j];x <= m;x ++)
f[i][j][x] += f[i - 1][k][x - c[j]];
}
}
}
}
ll res = 0;
for(int i = 1;i <= cnt;i ++)
res += f[n][i][m];//记得累加答案
cout << res;
return 0;
}
补充练习
这道题其实是LIS问题加上输出方案数,比较麻烦的就是统计方案。首先按照题目的要求,要去重,而这一步可以在进行动规的时候写。去重的话,优先保留后面的数,因为如果两个数一样,那么很明显第二个数可能得到的长度更大。
#include<bits/stdc++.h>
using namespace std;
const int N = 5003;
int f[N];
int cnt[N];
int a[N];
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++)
scanf("%d",&a[i]);
int len_max = 0;
for(int i = 1;i <= n;i ++)
{
f[i] = 1;//对于没一个数,它的长度至少为1
for(int j = 1;j < i;j ++)
{
if(a[i] < a[j])
f[i] = max(f[i],f[j] + 1);//状态转移
else if(a[i] == a[j])
f[j] = -10;//去重
}
len_max = max(len_max,f[i]);//记录最大长度
if(f[i] == 1)
cnt[i] = 1;//此处特判,为1说明满足以i为结尾的长度为1的方案数只有一种,而如果不是1的话,它自己本身就不能作为一种方案,而只能用前面合法的状态相加
else for(int j = 1;j < i;j ++)
if(f[i] == f[j] + 1 && a[j] > a[i]) cnt[i] += cnt[j];//要将前面合法的状态加起来
}
int ans = 0;
for(int i = 1;i <= n;i ++) if(f[i] == len_max) ans += cnt[i];//有可能存在多个以不同的数结尾,但是长度都是最长的,所以此处还是要加一遍
cout << len_max << " " << ans;
return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int N = 81;
string a,b;
int f[N][N];
int len;
int la,lb;
char ans[N];
void print()
{
for(int i = 1;i<= len;i ++)
cout << ans[i];
puts("");
}
void dfs(int i,int j,int step)
{
// cout << 1;
if(step > len)
{
print();
return;
}
if (a[i] == b[j])//如果当前搜到的两个数相等,那么它们就必须被选,因为如果不选这两个,后面的答案不可能更优
{
ans[step] = a[i];
dfs(i + 1, j + 1, step + 1);//记得增加搜索的编号和数量
}
else
{
for (int k = 0; k < 26; k ++ )//将a~z每个数顺序遍历,这样可以保证得到的序列是按照字典序排列的
{
int na = -1;
int nb = -1; //na,nb用来记录下一个相同的字母是哪一个
for (int x = i; x < la; x ++ )//遍历a串
if (a[x] == 'a' + k)
{
na = x;
break;
}
for (int x = j; x < lb; x ++ )//遍历b串
if (b[x] == 'a' + k)
{
nb = x;
break;
}
if (na != -1 && nb != -1 && f[na][nb] == f[i][j] + 1)//这里继续递归下去的条件:首先必须na,nb存在,其次,由于f[i][j]不一样,而f[na][nb]一样,所以在这里
//的值要等于f[na][nb] == f[i][j] + 1,不能比这个数大,如果比它大的话,就说明中间漏掉了一些相同的数,这种方案就不一定属于最优的
{
dfs(na, nb, step);
}
}
}
}
int main()
{
cin >> a >> b;
la = a.size(),lb = b.size();
for(int i = 0;i < la;i ++)
for(int j = 0;j < lb;j ++)
{
f[i][j] = max(f[i - 1][j],f[i][j - 1]);
if(a[i] == b[j])
f[i][j] = max(f[i - 1][j - 1] + 1,f[i][j]);
}//先dp求最大子序列的长度
len = f[la - 1][lb - 1];
dfs(0,0,1);//深搜
return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10,M = 1 << 5;
int f[N][M];//代表当围栏从i到i+4时,状态为j时的总的最多小朋友高兴数
int num[N][M];//代表当围栏从i到i+4时,状态为j时的小朋友高兴数,注意实际上j的顺序是第0位对应i,以此类推
int n,m;
void init()
{
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i ++)
{
int e,f,l,fear = 0,like = 0,a,b;
scanf("%d%d%d",&e,&f,&l);
for(int j = 1;j <= f;j ++)
{
scanf("%d",&a);
a = (a - e + n) % n;
fear |= 1 << a;//1代表害怕的动物数。
}
for(int j = 1;j <= l;j ++)
{
scanf("%d",&b);
b = (b - e + n) % n;
like |= 1 << b;
}
for(int j = 0;j < 32;j ++)
if((j & fear) || (~j & like)) ++ num[e][j];//这里看仔细一点
}
}
int main()
{
init();
int ans = 0;
for(int i = 0;i < 32;i ++)//枚举每一次开始的状态
{
memset(f[0],-0x3f,sizeof f[0]);//初始化成极小数
f[0][i] = 0;//第零个围栏的状态为0(实际上对应的是第n个围栏的状态)
for(int j = 1;j <= n;j ++)//枚举每一行
for(int k = 0;k < 32;k ++)//枚举每一种状态
f[j][k] = max(f[j - 1][(k & 15) << 1],f[j - 1][(k & 15) << 1 | 1] ) + num[j][k];
//此处说一下转移方程,每一种状况都可以由它上一个状态转移过来,转移时分为上一个状态第0位为0和1(不去掉和去掉),因为我们上面已经说过状态对应的储存顺序,所以就用((k & 15) << 1),然后第零位为0或1,加上当前这五个围栏
if(ans < f[n][i]) ans = f[n][i];//结尾的状态与第0个状态相同,所以用f[n][i]
}
printf("%d",ans);
return 0;
}