文章目录
- 前言
- [P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles](https://www.luogu.com.cn/problem/P1216)(入门题)
- [P1434 [SHOI2002]滑雪](https://www.luogu.com.cn/problem/P1434)(记忆化搜索)
- [P2196 挖地雷](https://www.luogu.com.cn/problem/P2196)(DP,记忆化搜索)
- [P4017 最大食物链计数](https://www.luogu.com.cn/problem/P4017)(DP,拓扑排序)
- [P1048 采药](https://www.luogu.com.cn/problem/P1048)(01背包)
- [P1616 疯狂的采药](https://www.luogu.com.cn/problem/P1616)(完全背包)
- [P1802 5倍经验日](https://www.luogu.com.cn/problem/P1802)(01背包变形)
- [P1002 过河卒](https://www.luogu.com.cn/problem/P1002)(DP,递推)
前言
动态规划是一种重要的思维方法,通过利用已有的子问题信息高效求出当前问题的最优解。
题单地址:https://www.luogu.com.cn/training/211#problems
P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles(入门题)
思路:动态规划 a + b 题,分别有从上到下DP(需要遍历最大值)和从下到上DP(唯一),不说了,直接上代码。
状态转移:
// 从上到下DP
#include <iostream>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];
int f[N][N];
int main()
{
cin>>n;
for (int i = 1; i <= n;i ++ )
for(int j = 1; j <= i;j ++ )
cin >> a[i][j];
for (int i = 1;i <= n;i ++ )
for(int j = 0;j <= i + 1; j ++ ) // 多初始化边界
f[i][j] = -INF;
f[1][1] = a[1][1];
for (int i = 2; i <= n;i ++ )
for (int j = 1; j <= i; j ++ )
f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);
int res = -INF;
for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
// 从下到上DP
#include <iostream>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];
int f[N][N];
int main()
{
cin >> n;
for(int i =1;i<=n;i++)
for(int j=1;j<=i;j++)
cin >> a[i][j];
for(int i =1;i<=n;i++)
for(int j=1;j<=i;j++)
if(i==n) f[i][j] = a[i][j]; // 初始化
else f[i][j]=-INF;
for(int i=n-1;i>=1;i--)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i+1][j],f[i+1][j+1]) + a[i][j];
cout<<f[1][1]<<endl;
return 0;
}
P1434 [SHOI2002]滑雪(记忆化搜索)
思路:状态转移条件:
w[x][y] > w[a][b]
#include <iostream>
using namespace std;
const int N = 110;
int n,m;
int w[N][N];
int f[N][N];
int dx[4]={-1,0,1,0}, dy[4] = {0,1,0,-1};
int dfs(int x,int y)
{
if(f[x][y]) return f[x][y];
f[x][y]=1;
for(int i=0;i<4;i++)
{
int a=x +dx[i],b=y + dy[i];
if(a>=1 && a<=n && b>=1 && b<=m)
{
if(w[x][y] > w[a][b])
{
dfs(a,b);
f[x][y] = max(f[x][y],f[a][b] + 1);
}
}
}
return f[x][y];
}
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin >> w[i][j];
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
res=max(res,dfs(i,j));
cout<<res<<endl;
return 0;
}
P2196 挖地雷(DP,记忆化搜索)
思路:本题需要额外记录路径,记录路径的常用方法:开一个
pre[]
前缀数组,记录当前状态是由哪个状态转移过来的,下面给出正推,逆推,记忆化搜索的代码
状态转移条件:g[i][j] == 1
,表示i
与j
直接存在边
代码1(正推,需要缓存数组)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 25;
int n;
int a[N];
int g[N][N];
int f[N];
int pre[N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i],f[i] = a[i];
for(int i=1;i<=n-1;i++)
for(int j=i+1;j<=n;j++)
cin >> g[i][j];
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
{
if(g[i][j]) // 当g[i][j] == 1时,状态转移 f[j] = f[i] + a[j];
{
if(f[j] < f[i] + a[j])
{
f[j] = f[i] + a[j];
pre[j] = i; // 记录前缀
}
//f[j] = max(f[j],f[i] + a[j]);
}
}
// 以下是输出*************************************************
int t =0; // 记录最大值的下标
for(int i=1;i<=n;i++)
if(f[t] < f[i])
{
t=i;
}
int ans = f[t];
// 正着推需要缓存,倒着推会比较好
vector<int> res;
while(pre[t] != 0)
{
res.push_back(t);
t = pre[t];
}
res.push_back(t);
reverse(res.begin(),res.end());
for(int i=0;i<res.size();i++) cout<<res[i]<<" ";
cout<<endl;
cout<<ans;
return 0;
}
代码2(逆推)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 25;
int n;
int a[N];
int g[N][N];
int f[N];
int pre[N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i],f[i] = a[i];
for(int i=1;i<=n-1;i++)
for(int j=i+1;j<=n;j++)
cin >> g[i][j],g[j][i] = g[i][j];// 无向边,倒着搜需要用到
// 倒着搜比较好回溯路径
for(int i=n;i>=1;i--)
for(int j=i-1;j>=1;j--)
{
if(g[i][j]) // 当g[i][j] == 1时,状态转移 f[j] = f[i] + a[j];
{
if(f[j] < f[i] + a[j])
{
f[j] = f[i] + a[j];
pre[j] = i; // 记录前缀
}
//f[j] = max(f[j],f[i] + a[j]);
}
}
// 以下是输出*************************************************
int t =0; // 记录最大值的下标
for(int i=1;i<=n;i++)
if(f[t] < f[i])
{
t=i;
}
int ans = f[t];
// 正着推需要缓存,倒着推会比较好,以下是倒着搜就不用缓存
vector<int> res;
while(pre[t] != 0)
{
cout<<t<<" ";
t=pre[t];
}
cout<<t<<endl;
cout<<ans;
return 0;
}
代码3(记忆化搜索)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 25;
int n;
int a[N];
int g[N][N];
int f[N];
int ne[N];
int dfs(int u) // 返回这个点能挖到地雷的数量
{
if(f[u]) return f[u]; // 记忆化搜索
f[u] = a[u];
int maxv = 0;
for(int i = u + 1;i <= n; i ++ )
if(g[u][i])
{
if(dfs(i) > maxv)
{
maxv = dfs(i);
ne[u] = i;
}
//maxv = max(maxv,dfs(i)); // 取最大
}
f[u] += maxv;
return f[u];
}
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n-1;i++)
for(int j=i+1;j<=n;j++)
cin >> g[i][j];
int res=0,t=-1;
for(int i=1;i<=n;i++)
{
if(res < dfs(i))
{
res = dfs(i);
t = i;
}
}
while(ne[t] != t)
{
cout<<t<<" ";
t = ne[t];
}
cout<<endl;
cout<<res<<endl;
return 0;
}
P4017 最大食物链计数(DP,拓扑排序)
思路:状态转移:对于
t
的所有出边j
,f[j] = (f[t] + f[j]) % mod
不说了,拓扑排序模板学一手好吧
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int N = 500010, mod = 80112002;
int n,m;
int h[N],e[N],ne[N],idx;
int d[N],out[N]; // 记录每个点的入度,出度
int f[N]; // 到这个点的食物链条数
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a] ,h[a] = idx ++ ;
}
void topsort()
{
queue<int> q;
// 寻找入度为0的点,生产者
for(int i=1;i<=n;i++)
if(d[i] == 0)
{
q.push(i);
f[i] = 1;
}
while(q.size())
{
int t = q.front();
q.pop();
for(int i=h[t];i!=-1;i=ne[i]) // 遍历所有出边
{
int j = e[i];
d[j] --;
f[j] = (f[t] + f[j]) % mod; // 状态转移
if(d[j] == 0) q.push(j);
}
}
}
int main()
{
memset(h,-1,sizeof h); // 记得初始化头结点
scanf("%d%d",&n,&m);
int a,b;
while(m--)
{
scanf("%d%d",&a,&b);
add(a,b);
d[b] ++ ;
out[a] ++ ;
}
topsort(); // 忘记调用了~~
int res=0;
for(int i=1;i<=n;i++)
if(out[i] == 0) res = (res + f[i]) % mod; // 出度为0的点为消费者
printf("%d\n",res);
return 0;
}
P1048 采药(01背包)
思路:01背包裸题,时间看成体积。
这里提供一个优化空间后的写法,注意体积从大到小循环,与下文完全背包区别。
#include <iostream>
using namespace std;
const int N = 100010;
int n,m;
int v[N],w[N];
int f[N];
int main()
{
cin >> m >> n;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j] = max(f[j],f[j-v[i]] + w[i]);
cout<<f[m];
}
P1616 疯狂的采药(完全背包)
思路:完全背包裸题,同样这里提供一种优化空间之后的写法,
体积从小到大循环
(完全背包比较特别,可特殊记忆,一般来说优化空间都是利用滚动数组优化,体积从大到小。)
#include <iostream>
using namespace std;
const int N = 100010;
int n,m;
int v[N],w[N];
int f[N];
int main()
{
cin >> m >> n;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++) // 只有这里与01背包不同
f[j] = max(f[j],f[j-v[i]] + w[i]);
cout<<f[m];
}
P1802 5倍经验日(01背包变形)
思路:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1010;
int n,m;
int lose[N],win[N],use[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> lose[i] >> win[i] >> use[i];
for(int i=1;i<=n;i++)
{
// j >= use[i],有两种选择
for(int j=m;j>=use[i];j--) f[j] = max(f[j-use[i]] + win[i],f[j] + lose[i]); // 如果够药水,有两种选择
// j < use[i],一种选择
for(int j = use[i]-1;j>=0;j--) f[j] += lose[i];
}
//cout<<f[x] * 5 <<endl;
printf("%lld",5ll * f[m]); // 爆int
return 0;
}
P1002 过河卒(DP,递推)
思路:
状态表示f[i][j] : 从起点(0,0)到(i,j)的路径条数;状态计算:f[i][j] = f[i-1][j] + f[i][j-1]
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 25;
int n,m,x,y;
long long f[N][N];
bool use[N][N]; // 马控制的点
int dx[8]={-1,-2,-2,-1,1,2,2,1}, dy[8]={-2,-1,1,2,2,1,-1,-2};
int main()
{
cin >> n >> m >> x >> y;
use[x][y] = true;
for(int i=0;i<8;i++)
{
int a=x + dx[i], b = y + dy[i];
use[a][b] = true;
}
f[0][0]=1;
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++)
{
if(i) f[i][j] += f[i-1][j];
if(j) f[i][j] += f[i][j-1];
if(use[i][j]) f[i][j] = 0; // 控制点不能走
}
printf("%lld",f[n][m]); // 爆int
return 0;
}
优化空间之后的写法(滚动数组优化,直接删去第一维)
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 25;
int n,m,x,y;
long long f[N];
bool use[N][N]; // 马控制的点
int dx[8]={-1,-2,-2,-1,1,2,2,1}, dy[8]={-2,-1,1,2,2,1,-1,-2};
int main()
{
cin >> n >> m >> x >> y;
use[x][y] = true;
for(int i=0;i<8;i++)
{
int a=x + dx[i], b = y + dy[i];
use[a][b] = true;
}
f[0]=1;
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++)
{
if(j) f[j] += f[j-1];
if(use[i][j]) f[j] = 0; // 控制点不能走
}
printf("%lld",f[m]); // 爆int
return 0;
}