目录
Frog 1(AT_dp_a)
题意简述
有块石头,第
块石头的高度为
.
一只青蛙从第块石头开始跳跃,它可以往后跳一个或两个石头,且花费为
求当跳到第块时,总花费的最小值.
思路
很容易想到状态转移方程 :
递推至第项后输出即可.
代码
#include<bits/stdc++.h>
using namespace std;
int n;
int h[100005] , dp[100005];
int main(){
cin >> n ;
for(int i = 1 ; i <= n ; i++) {
cin >> h[i];
}
dp[1] = 0 , dp[2] = abs(h[2] - h[1]);
for(int i = 3 ; i <= n ; i++) {
dp[i] = min(dp[i-2] + abs(h[i] - h[i-2]) , dp[i-1] + abs(h[i] - h[i-1]));
}
cout << dp[n];
return 0;
}
Frog 2(AT_dp_b)
题意简述
有块石头,第
块石头的高度为
.
一只青蛙从第块石头开始跳跃,它可以往后
到
个石头,且花费为
求当跳到第块时,总花费的最小值.
思路
与上道题的不同点是转移项数变多了,所以需要对第到
项进行遍历
转移方程 :
递推至第项后输出即可.
代码
#include<bits/stdc++.h>
using namespace std;
int n , k;
int h[100005] , dp[100005];
int main(){
memset(dp, 0x3f , sizeof dp);
cin >> n >> k;
for(int i = 1 ; i <= n ; i++) {
cin >> h[i];
}
dp[1] = 0;
for(int i = 2 ; i <= n ; i++) {
for(int j = max(0 , i - k) ; j <= i - 1 ; j++) {
dp[i] = min(dp[i] , dp[j] + abs(h[j] - h[i]));
}
}
cout << dp[n];
return 0;
}
Vacation(AT_dp_c)
题意简述
给定天,太郎在每天可以进行三种操作:
其中,在第天操作1可增加
点快乐值 , 操作2可增加
点快乐值 , 操作3可增加
点快乐值
需要注意,太郎不能连续两天进行相同的操作
求天结束后 , 太郎快乐值总值的最大值
思路
属于基础dp ,考虑为 数组增加一维
令 表示第
天进行
操作的情况下所获得快乐值总值的最大值
则显然可以得到状态转移方程 :
代码
#include<bits/stdc++.h>
using namespace std;
int n;
int dp[3][100005];
int a[100005] , b[100005] , c[100005];
int main(){
cin >> n ;
for(int i = 1 ; i <= n ; i++){
cin >> a[i] >> b[i] >> c[i];
dp[0][i] = max(dp[1][i-1] , dp[2][i-1]) + a[i];
dp[1][i] = max(dp[0][i-1] , dp[2][i-1]) + b[i];
dp[2][i] = max(dp[1][i-1] , dp[0][i-1]) + c[i];
}
cout << max(dp[0][n] , max(dp[1][n] , dp[2][n]));
return 0;
}
Knapsack 1(AT_dp_d)
题意简述
给定 件物品 , 每件物品的价值为
, 重量为
, 从中选择若干件物品 , 且总重量不超过
, 求总价值的最大值。
思路
属于很经典的背包dp , 正常01背包即可 ,令 表示选到第
件物品且总重为
时的最大价值,状态转移方程如下:
代码
#include<bits/stdc++.h>
using namespace std;
int n , W;
long long dp[105][100005];
long long a[105] , w[105];
int main(){
cin >> n >> W;
for(int i = 1 ; i <= n ; i++){
cin >> w[i] >> a[i];
}
for(int i = 1 ; i <= n ; i++){
for(int j = W ; j >= 0 ; j--){
if(j >= w[i]){
dp[i][j] = max(dp[i-1][j-w[i]] + a[i] , dp[i-1][j]);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
cout << dp[n][W];
return 0;
}
Knapsack 2(AT_dp_e)
题意简述
给定 件物品 , 每件物品的价值为
, 重量为
, 从中选择若干件物品 , 且总重量不超过
, 求总价值的最大值。(
)
思路
相比上道题 ,这道题中 的范围提高到了
,这意味着我们无法通过枚举
来解决 , 但同时
的数量级降低到了
, 所以我们可以考虑令
表示枚举到第
个物品时令价值为
时的最小重量 , 则可以得到状态转移方程:
最后对于进行倒序枚举 , 判断是否合法即可。
代码
#include<bits/stdc++.h>
using namespace std;
int n ;
long long W;
long long dp[105][100005];
long long w[105] , v[105];
long long V;
int main(){
memset(dp , 0x3f , sizeof dp);
cin >> n >> W;
for(int i = 1 ; i <= n ; i++){
cin >> w[i] >> v[i];
V += v[i];
}
dp[0][0] = 0;
for(int i = 1 ; i <= n ; i++){
for(int j = V ; j >= 0 ; j--){
if(j >= v[i]){
dp[i][j] = min(dp[i-1][j] , dp[i-1][j-v[i]] + w[i]);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
for(int i = V ; i >= 0 ; i--){
if(dp[n][i] <= W){
cout << i ;
return 0;
}
}
return 0;
}
LCS(AT_dp_f)
题意简述
给定两个字符串 和
,求出它们的最长公共子序列。
思路
很经典的线性dp题目 , 这道题我们可以考虑先求出LCS的长度 , 再根据其找到LCS。
我们可以令 表示
的前
项 和
的前
项的
长度
那么很显然 , 状态转移方程 :
由此 , 即为
的长度。
求出长度后 , 我们可以考虑进行反向 , 通过状态转移方程反向推导即可。
代码
#include<bits/stdc++.h>
using namespace std;
int dp[3005][3005];
int len1 , len2 ;
string s , t;
void dfs(int i , int j){
if(i == 0 || j == 0 || dp[i][j] == 0){
return;
}
if(s[i] == t[j]){
dfs(i - 1 , j - 1);
cout << s[i];
}
else if(dp[i-1][j] == dp[i][j]) dfs(i - 1 , j);
else dfs(i , j - 1);
}
int main(){
cin >> s >> t;
len1 = s.size();
len2 = t.size();
s = ' ' + s;
t = ' ' + t;
for(int i = 1 ; i <= len1 ; i++){
for(int j = 1 ; j <= len2 ; j++){
if(s[i] == t[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j] , dp[i][j-1]);
}
}
dfs(len1 , len2);
return 0;
}
Longest Path(AT_dp_g)
题意简述
给出 个顶点
条边的有向无环图
, 求出该图中最长的路径长度。
思路
题目要求最长的路径长度 , 我们可以考虑对其进行分层 , 固定一个节点为最顶层 , 那么当走到最底层所走路径长度自然就是最长长度 , 对图上的每一个子图分别拓扑即可。
代码
#include<bits/stdc++.h>
using namespace std;
int n , m , ans;
int rd[100005];
int head[100005] , ne[100005] , ver[100005] , tot = -1 , dist[100005];
bool vis[100005];
void add(int x , int y){
tot++;
ver[tot] = y;
ne[tot] = head[x];
head[x] = tot;
}
void topsort(int x){
queue<int> q;
q.push(x);
vis[x] = 1;
dist[x] = 0;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = head[u] ; i != -1 ; i = ne[i]){
int v = ver[i];
vis[v] = 1;
dist[v] = max(dist[v] , dist[u] + 1);
//cout << v << " " << dist[v] << " " << u << " " << dist[u] << "\n";
rd[v]--;
if(rd[v] == 0) q.push(v);
}
}
}
int main(){
memset(head, -1,sizeof head);
cin >> n >> m ;
for(int i = 1 ; i <= m ; i++){
int x , y;
cin >> x >> y ;
rd[y] ++;
add(x , y);
}
for(int i = 1 ; i <= n ; i++){
if(rd[i] == 0 && !vis[i]){
topsort(i);
}
}
for(int i = 1 ; i <= n ; i++){
ans = max(ans , dist[i]);
}
cout << ans;
return 0;
}
Grid 1(AT_dp_h)
题意简述
给出一个行
列的矩阵 , 包含两种元素
表示可以走 ,
表示不能走,求从
走到
的路径总数。
思路
比较基础的二维 , 可以令 表示走到第
行
列时的路径总数。
则很简单可以获得状态转移方程
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int n , m ;
char ma[1005][1005];
long long dp[1005][1005];
int main(){
cin >> m >> n;
for(int i = 1 ; i <= m ; i++){
for(int j = 1 ; j <= n ; j++){
cin >> ma[i][j];
}
}
dp[1][1] = 1;
for(int i = 1 ; i <= m ; i++){
for(int j = 1 ; j <= n ; j++){
if(i == 1 && j == 1) continue;
if(ma[i][j] == '#') continue;
dp[i][j] = dp[i][j-1] + dp[i-1][j];
dp[i][j] %= mod;
}
}
cout << dp[m][n];
return 0;
}
Coins(AT_dp_i)
题意简述
给定一个正奇数 , 有
枚硬币 , 其中第
枚硬币出现正面的概率为
,出现反面的概率是
,求正面个数多于反面的次数。
思路
比较经典的数学期望 ,我们可以考虑令
表示前
个硬币有
个向上的概率 ,那么状
态转移方程很显然 ,
求得后按题意输出即可。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
double p[3005] , dp[3005][3005];
int main(){
cin >> n ;
for(int i = 1 ; i <= n ; i++){
cin >> p[i];
}
dp[0][0] = 1;
for(int i = 1 ; i <= n ; i++) {
for(int j = 0 ; j <= n ; j++) {
dp[i][j] = dp[i - 1][j] * (1 - p[i]) + dp[i - 1][j - 1] * p[i];
}
}
double ans = 0;
for(int i = n / 2 + 1 ; i <= n ; i++){
ans += dp[n][i];
}
cout << fixed << setprecision(10) << ans;
return 0;
}
Sushi(AT_dp_j)
题意简述
给出 道菜 , 其中第
道菜上有
块寿司 (
) 。
从中随机抽取一个数
,如果第
道菜上还有寿司 , 就吃掉一块 , 否则就不吃。
求吃完所有寿司的期望操作次数。
思路
也是一道数学期望dp ,在这道题中 , 显然普通的dp会超时 , 所以我们考虑以空间换时间 , 对dp数组进行换维 ,令 剩余
盘
个的寿司 ,
盘
个的寿司 ,
盘
个的寿司时的期望操作次数 , 那么可以得到一个很长的状态转移方程 :
我们对其进行化简可以得到:
遍历求解即可。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
double dp[305][305][305];
int cnta , cntb , cntc;
int plate[305];
int main(){
cin >> n ;
for(int i = 1 ; i <= n ; i++) {
cin >> plate[i];
if(plate[i] == 1) cnta ++;
if(plate[i] == 2) cntb ++;
if(plate[i] == 3) cntc ++;
}
for(int k = 0 ; k <= n ; k++){
for(int j = 0 ; j <= n ; j++){
for(int i = 0 ; i <= n ; i++){
if(i+j+k == 0) continue;
if(i) dp[i][j][k] += dp[i-1][j][k] * i / (i + j + k);
if(j) dp[i][j][k] += dp[i+1][j-1][k] * j / (i + j + k);
if(k) dp[i][j][k] += dp[i][j+1][k-1] * k / (i + j + k);
dp[i][j][k] += 1.0*n / (i + j + k);
}
}
}
cout << fixed << setprecision(10) << dp[cnta][cntb][cntc];
return 0;
}
Stones(AT_dp_k)
题意简述
给定 个正整数
, 有
块石头,两个玩家轮流选择一个正整数
, 并从石头中移除
块 , 当玩家无法进行操作时判负 , 求先手能否获胜。
思路
这是一道博弈论dp , 我们可以从胜负状态出发思考 , 当当前剩余 块石头时 ,当前操作者一定必败 , 所以如果当前的状态可以转化为必败状态 , 当前操作者一定必胜。
那么我们可以令表示剩余
块石头时当前操作者的胜负 , 那么显然
为必胜当且仅当存在
且
为必败。
代码
#include<bits/stdc++.h>
using namespace std;
int n , k ;
int a[105];
bool dp[100005];
int main(){
cin >> n >> k;
for(int i = 1 ; i <= n ; i++){
cin >> a[i];
}
for(int i = 1 ; i <= k ; i++){
for(int j = 1; j <= n ; j++){
if(i - a[j] < 0) continue;
dp[i] |= !dp[i - a[j]];
}
}
if(dp[k] == 1) cout << "First";
else cout << "Second";
return 0;
}
Candies(AT_dp_m)
题意简述
有 个孩子分
颗糖果 , 其中第
个孩子必须要分到
颗糖果 , 求分享糖果的总方案数 , 并
。
思路
很经典的前缀和优化 dp ,我们很容易可以想到朴素的dp状态 ,令 表示考虑到第
个孩子 ,已经用了
颗糖的方案数 ,则显然有
注意到 , 我们可以用前缀和优化掉连续的
值之和 , 就有
.
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int n , k;
long long a[105];
long long sum[100005];
long long dp[105][100005];
int main(){
dp[0][0] = 1;
cin >> n >> k ;
for(int i = 1 ; i <= n ; i++) {
cin >> a[i];
}
sum[0] = 1;
for(int i = 1 ; i <= k ; i++) {
sum[i] = sum[i - 1];
sum[i] %= mod;
}
for(int i = 1 ; i <= n ; i++) {
for(int j = 0 ; j <= k ; j++) {
int l = max(0ll , j - a[i]);
if(l <= 0) dp[i][j] = sum[j];
else dp[i][j] = sum[j] - sum[l - 1] + mod;
dp[i][j] %= mod;
}
sum[0] = dp[i][0];
for(int j = 1 ; j <= k ; j++) {
sum[j] = sum[j - 1] + dp[i][j];
sum[j] %= mod;
}
}
cout << dp[n][k];
return 0;
}
Slimes(AT_dp_n)
题意简述
有 个数 , 要进行
次操作 , 每次操作会选择相邻的两个数进行合并求和 , 操作的代价也为这个和 ,求总小的总代价。
思路
这是一道比较常规的区间dp ,我们可以令 表示
到
合并的最小代价 , 我们可以外层枚举区间长度 , 第二层枚举左端点 , 求出右端点后枚举分割点
, 则很显然可以获得状态转移方程
由此求出 即可。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
long long a[405],dp[405][405];
long long sum[405];
int main(){
for(int i=1;i<=400;i++){
for(int j=1;j<=400;j++){
dp[i][j]=LONG_LONG_MAX;
}
}
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
dp[i][i]=0;
}
for(int len=2;len<=n;len++){
for(int left=1;left+len-1<=n;left++){
int right=left+len-1;
for(int k=left;k<right;k++){
dp[left][right]=min(dp[left][right],dp[left][k]+dp[k+1][right]+sum[right]-sum[left-1]);
}
}
}
cout<<dp[1][n];
return 0;
}
Matching(AT_dp_o)
题意简述
给定二分图 , 两个集合都有 个点 ,
表示第一个集合第
个点 与第二个集合第
个点连边 。求二分图完备匹配数 ,答案对 取模。
思路
这是一道较有难度的状压dp题目 , 我们可以考虑枚举 个点和当前集合
中点的匹配数量 , 如果枚举成功 , 就更新答案和集合 , 同时答案要加上上上个集合的数量。
显然 , 想要匹配成功需要满足两个条件:
1.当前的点与集合 中的点有连边
2.当前的点与集合中的点都没有被匹配过
我们可以设 表示
个集合
中的点与集合
中的点所构成的集合
的匹配数量 ,令
表示集合中已经有的女生个数 , 我们可以选择枚举男生个数和女生集合 , 则要满足
才能继续更新 。
此时再找一个集合 中的点
,则要满足
且
。
那么我们就可以得到状态转移方程 :
最终答案即为 ,输出即可。
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int n;
int a[25][25];
long long dp[25][1<<21];
int main(){
cin >> n ;
for(int i = 0 ; i < n ; i ++){
for(int j = 0 ; j < n ; j++){
cin >> a[i][j];
}
}
dp[0][0] = 1;
for(int i = 0 ; i < n ; i++) {
for(int j = 0; j < (1<<n) ;j++){
if(__builtin_popcount(j) == i){
for(int k = 0 ; k < n ; k++) {
if(a[i][k] && (j & (1<<k)) == 0 ){
dp[i + 1][j | (1<<k)] = (dp[i + 1][j | (1<<k)] + dp[i][j]) %mod;
}
}
}
}
}
cout << dp[n][(1 << n) - 1];
return 0;
}
Independent Set(AT_dp_p)
题意简述
给定一棵 个节点的树,每一个点可以染成黑色或白色,任意两个相邻节点不能都是黑色,求方案数,结果对
取模。
思路
比较基础的树形dp ,我们可以默认节点 为树根,令
表示节点
染白色时的方案数 , 令
表示节点
染黑色时的方案数 , 那么状态转移方程不难推出:
那么在树上进行 同时求解即可。
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
int n;
int tot = -1 , head[200005] , ne[200005] , ver[200005];
long long dp[100005][2];
void add(int x , int y){
tot++;
ver[tot] = y;
ne[tot] = head[x];
head[x] = tot;
}
void dfs(int root , int father){
dp[root][0] = dp[root][1] = 1;
for(int i = head[root] ; i != -1 ; i = ne[i]){
int x = ver[i];
if(ver[i] == father) continue;
dfs(ver[i] , root);
dp[root][0] *= (dp[ver[i]][0] + dp[ver[i]][1]);
dp[root][0] %= mod;
dp[root][1] *= dp[ver[i]][0];
dp[root][1] %= mod;
}
}
int main(){
memset(head, -1 , sizeof head);
cin >> n ;
for(int i = 1 ; i < n ; i++) {
int x , y ;
cin >> x >> y;
add(x , y);
add(y , x);
}
dfs(1 , 0);
cout << (dp[1][0] + dp[1][1]) % mod;
return 0;
}
Walk(AT_dp_r)
题意简述
给出一张 个点有向简单图,给出邻接矩阵,求长度为
的路径条数,答案对
取模。
思路
比较典型的矩阵加速题目 , 对于原邻接矩阵 , 如果我们令 表示从
到
长度为
的路径条数 , 那么显然有
就可以推导出来答案为
但是显然时间复杂度超标 , 但是观察递推式我们可以发现 , 它相当于对邻接矩阵进行了一个矩阵乘法,又因为矩阵乘法满足结合律 , 所以我们就可以用矩阵快速幂对其进行优化。
设邻接矩阵为 , 则答案即为
中所有数字之和。
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int n;
long long k;
struct Matrix{
long long a[105][105];
int r , c ;
};
Matrix operator *(Matrix A , Matrix B){
Matrix C;
C.r = n , C.c = n;
memset(C.a , 0 , sizeof(C.a));
for(int i = 1 ; i <= A.r ; i++){
for(int j = 1 ; j <= B.c ; j++){
for(int k = 1 ; k <= A.c ; k++){
C.a[i][j] += (A.a[i][k] * B.a[k][j] % mod);
C.a[i][j] %= mod;
}
}
}
return C;
}
Matrix init(Matrix A){
for(int i = 1 ; i <= A.r ; i++){
for(int j = 1 ; j <= A.c; j++){
A.a[i][j] = (i == j);
}
}
return A;
}
Matrix quick_Power(Matrix A , long long k){
Matrix ans;
ans.c = n , ans.r = n;
ans = init(ans);
while(k){
if(k&1){
ans = ans * A;
}
A = A * A;
k = k >> 1;
}
return ans;
}
int main(){
Matrix A , T;
cin >> n >> k;
A.c = n , A.r = n , T.r = n , T.c = n;
for(int i = 1 ; i <= n ; i++) {
for(int j = 1 ; j <= n ; j++) {
cin >> A.a[i][j];
}
}
T = quick_Power(A , k);
long long ans = 0 ;
for(int i = 1 ; i <= T.r ; i++) {
for(int j = 1 ; j <= T.c ; j++) {
ans += T.a[i][j];
ans %= mod;
}
}
cout << ans;
return 0;
}
Grouping(AT_dp_u)
题意简述
对 个物品进行分组 ,如果第
个物品和第
个物品分在一组 , 会产生
的得分 ,要求最大化得分之和 , 其中
与
只计算一次。
思路
这也是一道状压dp的题目 , 相较 更为简单 , 我们可以考虑设
为集合
的最大得分 , 那么可以考虑先计算出不划分集合情况下的总得分 , 再对其进行枚举子集 ,就可以得到状态转移方程:
其中
表示
对于全集
的补集。
可以思考一下,在这个状态转移方程中我们需要逐个枚举子集并判断,显然是有些冗余的,是否有方法可以直接枚举子集呢 , 这里给出一条循环语句 :
在这条语句中 , 保证了
的值严格递减 , 同时保证了
,所以就不重不漏的完成了对子集的枚举。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
long long dp[1 << 20];
long long v[1 << 20];
long long a[20][20];
int main(){
cin >> n ;
for(int i = 0 ; i < n ; i++) {
for(int j = 0 ; j < n ; j++) {
cin >> a[i][j];
}
}
for(int k = 1 ; k < (1 << n) ; k++){
for(int i = 0 ; i < n ; i++) {
if((k >> i) & 1){
for(int j = i + 1 ; j < n ; j++) {
if((k >> j) & 1){
v[k] += a[i][j];
}
}
}
}
dp[k] = v[k];
}
for(int i = 0 ; i < (1 << n) ; i++){
for(int j = i & (i - 1) ; j ; j = i & (j - 1)) {
dp[i] = max(dp[i] , dp[j] + dp[j ^ i]);
}
}
cout << dp[(1 << n) - 1];
return 0;
}

680

被折叠的 条评论
为什么被折叠?



