DP入门
- 将一个大问题,化解为小问题,通过小问题的最优解进而推出大问题的最优解
最优化
- 记忆化,记搜时,开一个数组对已知进行记录
状态表示
- 有最优子结构:子问题的最优解可以有效构造出大问题的最优解
- 简洁性:尽可能简化状态
- 全面性:要统筹考虑问题
可以考虑从暴力DP写起
状转
- 类似分类讨论 考虑边界
复杂度
- 一般为状态数 ( imes)状转复杂度
优化
- 压缩维数
- 加速转移
最长上升子序列
(dp_i = {max(dp_j) + 1 | j<i ,a_j < a_i})
(n^2)枚举的暴力DP
P1091 合唱队型
题目传送门
要保留的人数 = 最长上升子序列 + 最长下降子序列 - 1
ans = n - 要保留的人数
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 1e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int t[N], f[N], g[N];
int main() {
int n = read();
for (int i = 1; i <= n; i++)
t[i] = read();
for (int i = 1; i <= n; i++) {
f[i] = 1;
for (int j = 1; j < i; j++) {
if (t[j] < t[i] && f[j] + 1 > f[i])
f[i] = f[j] + 1;
}
}
for (int i = n; i >= 1; i--) {
g[i] = 1;
for (int j = n; j > i; j--) {
if (t[j] < t[i] && g[j] + 1 > g[i])
g[i] = g[j] + 1;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(f[i] + g[i] - 1, ans);
}
printf("%d", n - ans);
system("pause");
return 0;
}
设(f_{i,a})表示第(i)位上已经放了(a)个乘号的最大价值
(f_{i,a} = max{f_{i,a} , f_{j,a-1} *cut_{j+1,i} })
(cut{x,y}) 表示 (x o y) 这一块的数字
然后写个高精度即可
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct node {
int v;
bool exi;
int c[N];
} cut[N][10], ans[N];
int n, k;
char s[N];
int a[N];
node culc(int l, int r) {
node e;
e.v = r - l + 1, e.exi = true;
for (int i = 1; i <= e.v; i++) {
e.c[i] = a[r - i + 1];
}
return e;
}
node mul(node e1, node e2) {
node emul;
emul.exi = true, emul.v = e1.v + e2.v - 1;
for (int i = 1; i <= emul.v; i++)
emul.c[i] = 0;
for (int i = 1; i <= e1.v; i++) {
for (int j = 1; j <= e2.v; j++) {
emul.c[i + j - 1] += e1.c[i] * e2.c[j];
}
}
int q = 0;
for (int i = 1; i <= emul.v; i++) {
emul.c[i] += q;
q = emul.c[i] / 10;
emul.c[i] %= 10;
}
while (q > 0) {
emul.c[++emul.v] = q % 10;
q /= 10;
}
return emul;
}
node Max(node e1, node e2) {
if (!e1.exi || e1.v < e2.v)
return e2;
if (!e2.exi || e2.v < e1.v)
return e1;
for (int i = e1.v; i >= 1; i--) {
if (e1.c[i] > e2.c[i])
return e1;
else if (e2.c[i] > e1.c[i])
return e2;
}
return e1;
}
int main() {
n = read(), k = read();
scanf("%s", s);
for (int i = 0; i < n; i++)
a[i + 1] = s[i] - '0';
for (int i = 1; i <= n; i++) {
ans[i].exi = false;
for (int j = 1; j <= k; j++)
cut[i][j].exi = false;
}
for (int i = 1; i < n; i++) {
cut[i][1] = culc(1, i);
for (int j = 2; j <= k; j++) {
for (int fr = j - 1; fr < i; fr++) {
if (cut[fr][j - 1].exi)
cut[i][j] = Max(cut[i][j],
mul(cut[fr][j - 1], culc(fr + 1, i)));
}
}
if (cut[i][k].exi) {
ans[i] = mul(cut[i][k], culc(i + 1, n));
}
}
node lastans;
lastans.exi = false;
for (int i = 1; i < n; i++) {
node temp = Max(ans[i], lastans);
lastans = temp;
}
for (int i = lastans.v; i >= 1; i--) {
printf("%d", lastans.c[i]);
}
system("pause");
return 0;
}
最长公共子序列
(dp_{i,j})表示(S串)前(i)为和(T串)前(j)位的最长公共子序列
- (dp_{i,j} = {dp_{i-1,j-1} + 1|S_i = T_j})
- (dp_{i,j} = {max(dp_{i-1,j},dp_{i,j-1} )|S_i != T_j})
- (ans = dp_{n,m})
notice:
为什么第一个转移没有取( ext{max})?仔细研究一下可以发现它的性质
最长公共上升子序列 LCIS
- (dp_{i,j})表示(S串)前(i)为和(T串)前(j)位的最长公共上升子序列 (t)表示LCIS的结尾位置
- (dp_{i,j} = {dp_{i- 1, j} | S_i != T_j})
- (dp_{i,j} = {max(dp_{i - 1,j} , dp_{i-1,t} + 1) | S_i = T_i })
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#define ll long long
using namespace std;
const int N = 1100;
int read() int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
int a[N], b[N];
int f[N][N] ,g[N][N];
int n ,m;
void print(int p) {
if(!p) return;
print(g[n][p]);
printf("%d ",b[p]);
}
int main() {
n = read();
for (int i = 1 ; i <= n ;i++) a[i] = read();
m = read();
for (int i = 1 ; i <= m ; i++) b[i] = read();
for(int i = 1 ; i <= n; i++) {
int t = 0;
for(int j = 1; j <= m ;j++) {
f[i][j] = f[i-1][j];
g[i][j] = g[i-1][j];
if(a[i] == b[j] && f[i- 1][t] + 1 > f[i][j]) {
f[i][j] = f[i- 1][t] + 1;
g[i][j] = t;
}
if(b[j] < a[i] && f[i - 1][j] > f[i - 1][t])
t = j;
}
}
int p = 0;
for(int i = 1 ; i <= m ;i++) {
if(f[n][i] > f[n][p]) p = i;
}
cout << f[n][p] <<"
";
print(p);
// system("pause");
return 0;
}
记搜
开数组记录已有的答案,减少递归的层数
滑雪
solution:
口胡一个做法; (n^2)枚举起点,会发现每一个点的能到达的点的个数是一定的,可以考虑记忆每个能到达的位置的个数,直接(n^3)暴力即可
code:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 110;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int mapp[N][N];
int ans[N][N], n, m;
const int dx[4] = {0, 0, 1, -1};
const int dy[4] = {1, -1, 0, 0};
int dfs(int x, int y) {
if (ans[x][y])
return ans[x][y];
ans[x][y] = 1;
for (int i = 0; i < 4; i++) {
if (x + dx[i] < 1 || x + dx[i] > n || y + dy[i] < 1 || y + dy[i] > m)
continue;
if (mapp[x][y] > mapp[x + dx[i]][y + dy[i]]) {
dfs(x + dx[i], y + dy[i]);
ans[x][y] = max(ans[x][y], ans[x + dx[i]][y + dy[i]] + 1);
}
}
return ans[x][y];
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
mapp[i][j] = read();
}
int ANS = -1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
ANS = max(dfs(i, j), ANS);
}
}
printf("%d", ANS);
system("pause");
return 0;
}
拓扑上DP
因为是( ext{DAG}) 会发现每个点所能到达的食物链的最底端是一定的
不是DAG也有这样的性质对每个搜索到的点所能到达的入度为零的点进行计数,从每个入度为零的点开始搜索,食物链是从头到尾全着才行对每次搜素统计答案即可
code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#define ll long long
using namespace std;
const int N = 2e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume;
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
int rd[N], cd[N];
queue<int> q;
ll dp[N];
ll dfs(int x) {
if (dp[x])
return dp[x];
ll ans = 0;
if (!cd[x] && rd[x])
ans++;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
ans += dfs(to);
}
dp[x] = ans;
return ans;
}
int main() {
int n = read(), m = read();
for (int i = 1, u, v, w; i <= m; i++) {
u = read(), v = read();
add_edge(u, v);
rd[v]++;
cd[u]++;
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
if (!rd[i])
ans += dfs(i);
}
printf("%lld", ans);
system("pause");
return 0;
}
序列DP
- (f_i)表示前(i)天的运输最小成本
- (f_i = min{f_j + w( j + 1 ,i) * (i - j) | j < i})
- 然后在图中跑一个最短路,求出(w(j+1,i))的权值,( ext{n<=100})暴力 (DP) 即可
上面的式子太抽象了,考虑一下一些细节如果写:
首先输入的时候处理一个(sig_{i,j})数组,表示第(i)个码头到第(j)能否经过这条道路,(1)表示不行(0)表示可以通过
再开一个(dis_{i,j})数组表示第([i,j])区间(指日期)内到达(m)的最短距离,考虑如何求得(dis)数组,暴力即可,因为要求多遍的最短路,重新开一个数组(sel)将(sig)里面所存的限制转移到(sel)内,转移完成就跑一遍短路,记录到(dis)内,(dis)数组所存的值就是上面转移方程中(w(j+1,i))
最后写一个(n^2)的状转即可
考虑一个问题,如果(dis)内存是一个极大值,即不能通过,怎么办?那就直接赋值为极大值,因为不能通过所以一定会被舍弃掉
notice:
- 注意每次跑最短路的时候最短路数组,标记数组也要清空
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#define ll long long
using namespace std;
const int N = 1120;
const int inf = 0x3f3f3f3f;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct Edge {
int from, to, dis, net;
} e[N << 1];
int head[N], nume;
void add_edge(int from, int to, int dis) {
e[++nume] = (Edge){from, to, dis, head[from]};
head[from] = nume;
}
struct node {
int po, dis;
bool operator<(const node& b) const { return dis > b.dis; }
};
priority_queue<node> q;
int Dis[N], dis[N][N], dp[N];
bool sig[N][N], sel[N];
int n, m, k, ee;
bool vis[N];
int dij() {
memset(Dis, 0x3f, sizeof(Dis));
memset(vis, 0, sizeof(vis));
Dis[1] = 0;
q.push((node){1, 0});
while (!q.empty()) {
node fr = q.top();
q.pop();
if (vis[fr.po])
continue;
vis[fr.po] = true;
for (int i = head[fr.po]; i; i = e[i].net) {
int to = e[i].to;
if (sel[to])
continue;
if (Dis[to] > Dis[fr.po] + e[i].dis) {
Dis[to] = Dis[fr.po] + e[i].dis;
if (!vis[to]) {
q.push((node){to, Dis[to]});
}
}
}
}
return Dis[m];
}
int main() {
memset(dis, 63, sizeof(dis));
n = read(), m = read(), k = read(), ee = read();
for (int i = 1, u, v, w; i <= ee; i++) {
u = read(), v = read(), w = read();
add_edge(u, v, w), add_edge(v, u, w);
}
int d = read();
for (int i = 1; i <= d; i++) {
int t = read(), x = read(), y = read();
for (int j = x; j <= y; j++)
sig[t][j] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
memset(sel, 0, sizeof(sel));
for (int l = i; l <= j; l++)
for (int r = 1; r <= m; r++)
sel[r] |= sig[r][l];
dis[i][j] = dij();
}
}
dp[0] = 0;
for (int i = 1; i <= n; i++) {
dp[i] = (dis[1][i] == inf) ? inf : dis[1][i] * i;
for (int j = 0; j < i; j++) {
if (dis[j + 1][i] == inf)
continue;
dp[i] = min(dp[i], dp[j] + dis[j + 1][i] * (i - j) + k);
}
}
printf("%d", dp[n]);
system("pause");
return 0;
}
-
一条木板:(g_{i,j})表示前(i)个格子刷了(j)次最多正确的格子
-
(g_{i,j} = max{g_{k,j-1} + w(k+1,i)~~ |~~ k< i})
-
多条木板: (f_{i,j})表示前(i)个木板刷(j)次的最大答案
-
(f_{i,j} = max{f_{i-1,k} + g_{i,m,j-k}})
最后一个转移方程中 (g) 多了一维,这一维的意思是第几块木板
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 70;
const int M = 3000;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int n, m, t;
char s[N * 10][M];
int g[N][N][M];
int dp[N][M];
int val(int id, int l, int r) {
int ans = 0;
for (int i = l; i <= r; i++)
ans += (s[id][i] == '0');
return max(ans, r - l + 1 - ans);
}
int main() {
n = read(), m = read(), t = read();
for (int i = 1; i <= n; i++)
scanf("%s", s[i] + 1);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int k = 1; k <= m; k++)
for (int l = k - 1; l <= j; l++)
g[i][j][k] = max(g[i][j][k], g[i][l][k - 1] + val(i, l + 1, j));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= t; j++)
for (int k = 0; k <= min(j, m); k++)
dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + g[i][m][k]);
int ans = 0;
for (int i = 1; i <= t; i++) {
ans = max(ans, dp[n][i]);
}
printf("%d", ans);
return 0;
}
其实可以发现里面有一部分( ext{val})这一块是可以通过前缀和而优化出来的,会降低一维的时间复杂度,但是也无所谓( ext{n,m<=50}),总体的时间复杂度貌似很低,最慢的一个我才跑了100ms完全可以随便写,不写前缀和优化也没问题
(dp_{i,j})表示到第(i)个字符还有(j)个左括号
分类讨论
如果第(i+1)为左括号,则能转移到(dp_{i+1,j+1})
如何第(i+1)为右括号,则能转移到(dp_{i+1,j-1})
如果第(i+1)为问号,则能转移到(dp_{i+1,j-1}与dp_{i+1,j+1})
会发现,第一维对于状转没有任何的限制或影响,省了去就好
/*
Author:Imy_isLy
知识点: 括号匹配,区间DP
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ui unsigned int
using namespace std;
const int N = 1e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
char s[N];
ui dp[N], ans = 1;
int cntp;
signed main() {
int n = read();
if (n & 1) {
printf("0");
return 0;
}
int mid = n >> 1;
dp[0] = 1;
scanf("%s", s + 1);
for (int i = 1; i <= n; i++)
if (s[i] == '?') {
for (int j = i >> 1; j >= i - mid && j; j--)
dp[j] += dp[j - 1];
} else
cntp++;
for (int i = 1; i <= mid - cntp; i++)
ans = ans * 25;
cout << ans * dp[mid];
// system("pause");
return 0;
}
卡特兰数
- 1 ~ n 由此进栈,求有多少种出栈序列
- 由n对括号形成的合法括号序列,求有多少种排列方式
单调队列优化DP
一个人比你小还比你强,那你就退役吧
简单DP ? 首先考虑(n^2)的转移方程,因为位置( ext{x})只能由位置([x-r,x-l])转移过来, 那就很明显了
(f_{i} = max{f_{j} | jin[i-r,i-l] } + a_i)
这时候考虑优化,会发现,(max{f_{j} | jin[i-r,i-l] })这一块只要取的是
最大值,很显然可以单调队列优化我也不知道为啥,每次只保留([i-r,i-l])内的最大值即可
/*
Author:Imy_isLy
知识点:单调队列优化DP
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 2e5 + 100;
const int inf = 2147483647;
//=============================================================
int f[N], n, l, r, ans = -inf;
int q[N], head = 1, tail = 0,a[N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
void insert(int i) {
for (; f[i] >= f[q[tail]] && tail >= head;)
tail--;
q[++tail] = i;
}
int query(int x) {
for (; q[head] + r < x;)
head++;
return q[head];
}
int main() {
memset(f, 128, sizeof(f));
f[0] = 0;
n = read(), l = read(), r = read();
for (int i = 0; i <= n; i++)
a[i] = read();
for (int i = l; i <= n; i++) {
insert(i - l);
int fr = query(i);
f[i] = f[fr] + a[i];
if (i + r > n)
ans = max(ans, f[i]);
}
cout << ans;
system("pause");
return 0;
}
区间DP
(dp_{i,j})表示区间([l,r]) 的( ext{DP})值
(dp_{i,j})表示区间([i,j])的最小体力值
(dp_{i,j} = min{dp_{i,k} + dp_{k,j}} + sum_{i,j})
/*
Author:Imy_isLy
知识点:区间DP
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 320;
const int inf = 0x3f3f3f3f;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int a[N], f1[N][N], f2[N][N], sum[N];
int main() {
int n = read();
for (int i = 1; i <= 2 * n; i++) {
if (i <= n)
a[i] = read();
a[i + n] = a[i];
sum[i] = sum[i - 1] + a[i];
}
memset(f1, 63, sizeof(f1));
for (int i = 0; i <= 2 * n; i++)
f1[i][i] = 0;
for (int len = 2; len <= n; len++) {
for (int l = 1; l <= 2 * n - len + 1; l++) {
int r = l + len - 1;
for (int k = l; k < r; k++) {
f1[l][r] = min(f1[l][r], f1[l][k] + f1[k + 1][r] + sum[r] - sum[l - 1]);
f2[l][r] = max(f2[l][r], f2[l][k] + f2[k + 1][r] + sum[r] - sum[l - 1]);
}
}
}
int ans1 = 1e9, ans2 = 0;
for (int i = 1; i <= n; i++)
ans1 = min(ans1, f1[i][i + n - 1]), ans2 = max(ans2, f2[i][i + n - 1]);
printf("%d
%d", ans1, ans2);
system("pause");
return 0;
}
有一个要求更高的石子合并,我不会……SDOI 2008
(dp_{i,j})表示区间([i,j])回文串的个数
(dp_{i,j} = dp_{i,j-1} + dp_{i-1,j} - dp_{i - 1,j - 1} + ([i,j]是否为回文串))
-
首先我们可以知道(dp_{i,i})肯定为1,然后以这个点为中心点,向左右两个方向同时扩展
-
这里要考虑奇偶两种情况,如果为偶数的回文串直接跑就OK,单独考虑奇数,右指针就要向右移一位,把中心位置空出来
-
然后求一个二维前缀和就好了
/*
Author:Imy_isLy
知识点: 区间DP ,二维前缀和
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 5e3 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
char s[N];
int dp[N][N];
int main() {
scanf("%s", s + 1);
int len = strlen(s + 1), q = read();
for (int mid = 1; mid <= len; mid++) {
for (int l = mid, r = mid; l && r <= len & s[l] == s[r]; l--, r++)
dp[l][r]++;
for (int l = mid, r = mid + 1; l && r <= len && s[l] == s[r]; --l, r++)
dp[l][r]++;
}
for (int i = 1; i <= len; i++)
for (int j = 1; j <= len; j++)
dp[i][j] += dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1];
while (q--) {
int l = read(), r = read();
printf("%d
", dp[r][r] - dp[l - 1][r] - dp[r][l - 1] + dp[l - 1][l - 1]);
}
system("pause");
return 0;
}
简化题面
给定一个字符串求至少添加几个 ((~~或者~~) ~~或者~~ [~~或者~~ ]) 能让 括号两两匹配
刚开始没有思路,题解是一种很重要的思想:正难则反
就可以的对这一整段区间进行动态规划,处理这一段区间有多少括号已经匹配,最后用字符串的长度减去 (f[1][len]) 就是答案
设 (f_{l,r})表示([l,r])区间内的匹配的括号的数量
然后跑一个枚举断点的动态规划就OK
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 120;
const int inf = 0x3f3f3f3f;
//=============================================================
char s[N];
int f[N][N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int main() {
scanf("%s", s + 1);
int len = strlen(s + 1);
for (int l = len; l; --l) {
for (int r = l + 1; r <= len; ++r) {
if ((s[l] == '(' && s[r] == ')') ||
(s[l] == '[' && s[r] == ']'))
f[l][r] = f[l + 1][r - 1] + 2;
for (int k = l; k < r; ++k) {
f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r]);
}
}
}
cout << (len - f[1][len]);
system("pause");
return 0;
}
没有血量上限。所以先打回血怪,这里分类一下,因为有的回血怪我们打不死
eg : 初始血量 5 , 掉血 100 回血10000
所以,回血怪先打掉血少的的,掉血怪先打回血多的就好了
/*
Author:Imy_isLy
知识点: 贪心
*/
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define int long long
using namespace std;
const int N = 1e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int n, z;
struct node {
int id, blood, d, a;
} enemy[N];
int ans[N];
bool cmp(node a, node b) {
if (a.blood * b.blood < 0)
return a.blood > b.blood;
else if (a.blood < 0 && b.blood < 0)
return a.a > b.a;
else
return a.d < b.d;
}
signed main() {
n = read(), z = read();
for (int i = 1, d, a; i <= n; i++) {
enemy[i].d = read(), enemy[i].a = read();
enemy[i].id = i, enemy[i].blood = enemy[i].a - enemy[i].d;
}
sort(enemy + 1, enemy + 1 + n, cmp);
for (int i = 1; i <= n; i++) {
z -= enemy[i].d;
if (z <= 0) {
puts("NIE");
system("pause");
return 0;
}
ans[i] = enemy[i].id;
z += enemy[i].a;
}
puts("TAK");
for (int i = 1; i <= n; i++) {
printf("%d ", ans[i]);
}
system("pause");
return 0;
}
- (dp_{i,j})表示区间([i,j])的变为回文串的最小代价
- 枚举一个区间的长度,然后(n^2)转移即可
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 2010;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int n, m;
char s[N];
int add[600], del[600], dp[N][N];
int main() {
n = read(), m = read();
scanf("%s", s + 1);
for (int i = 1; i <= n; i++) {
char ch;
cin >> ch;
add[(int)ch] = read(), del[(int)ch] = read();
}
for (int k = 1; k <= m; k++) {
for (int i = 1; k + i <= m; i++) {
int j = k + i;
dp[i][j] = min(dp[i + 1][j] + min(add[s[i]], del[s[i]]),
dp[i][j - 1] + min(add[s[j]], del[s[j]]));
if (s[i] == s[j]) {
if (j - i == 1)
dp[i][j] = 0;
else
dp[i][j] = min(dp[i][j], dp[i + 1][j - 1]);
}
}
}
printf("%d", dp[1][m]);
system("pause");
return 0;
}
区间DP处理环形问题
- 断环为链,然后把链复制一遍存到链的后面
一种思想并不一定是DP处理
区间DP的话 (dp_{i,i+n-1})取出最优解
我也不知道为啥循环的时候第二维枚举左端点……,为啥第一维枚举调不出来啊……怪……
大概如果第一维枚举长度的话就不出现这种这种问题了
(dp_{l,r} = max{dp_{l,k} + dp_{k+1,r} + a_l imes a_{k + 1} imes a_{r + 1} })
暴力DP 就好
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 320;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int a[N];
int f[N][N];
int main() {
int n = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
a[i + n] = a[i];
}
// for (int l = 1; l <= n; l++)
// for (int r = l + 1; r <= 2 * n && r - l + 1 <= n; r++)
// for (int k = l + 1; k < r; k++)
// f[l][r] =
// max(f[l][r], f[l][k] + f[k + 1][r] + a[l] * a[k + 1] * a[r + 1]);
for (int j = 2; j <= 2 * n - 1; j++)
for (int i = j - 1; i > 0 && j - i < n; i--)
for (int k = i; k < j; k++)
f[i][j] =
max(f[i][k] + f[k + 1][j] + a[i] * a[k + 1] * a[j + 1], f[i][j]);
int ans = -1e9;
for (int i = 1; i <= n; i++)
ans = max(ans, f[i][i + n - 1]);
printf("%d", ans);
system("pause");
return 0;
}
小结:
背包DP模型
记录方案数:
- (dp[i][j] = max( dp[i-1][j],dp[i-1][j+c[i]]+w[i]))状态转移过程中如果发现(dp[i-1][j] = dp[i-1][j+c[i]]+w[i])那么就是两种的方案数相加,否则的话就取最大的方案数
输出方案:
- 如果相等就随便选一个,如果不等就选大的那一组的选择,
两种题型都是开一个数组进行记录
(dp_{i,j} = max{(dp_{i - 1,j - c_i imes k + w_i imes k} | k <= cnt_i})
单调队列优化多重背包,根据同余类进行分类
背包中的第二维,(j)和能转移过来的(j - c imes k)在(mod~~c_i)意义下同余的,也就是说可以对于第二维按照(c_i)同余类进行分类
会发现不同类的互不影响,因为不同类之间不能够相互转化
设(f_i = dp_{i- 1,j * c_i + r}) r是我们枚举(c_i)的一个类
(dp_{i,j imes c_i + r} = max{f_k + (j - k) imes w_{i} | j - k <= cnt_i})
(dp_{i,j imes c_i + r} = max{f_k - k imes w_i | j - k <= cnt_i})
(dp_{i,j imes c_i + r} = max{dp_{i - 1, j imes c_{i} + r - (j - k) imes c_i} + (j - k) imes w_i | j - k <= cnt_i})
炒了一遍还是不是很会这种的做法
对于第二维在求方案数: 不能通过二进制拆分优化和单调队列优化
分组背包
- 背包空间为(V),每一组内有多个物品,每组中只能选一个,求最大发的费用
无语题…不知道为啥感觉这种DP很恶心,
里面有一个狠有意思的结论,最多有可能有一对夫妇住在同一个房间内,因为如果有两对,我们可以把这拆开,男男 女女这样分配
挺残忍的,不让人夫妻一块住
(dp_{i,j,k,0})表示前( ext{i})个房间住了( ext{j})个男人( ext{k})个女人,没有夫妻住同一个房间
(dp_{i,j,k,1})表示前( ext{i})个房间住了( ext{j})个男人( ext{k})个女人,有一对夫妻住同一个房间
仔细观察,其实发现,题目的本质是一个分组背包,一个房间就是一个物品,这个物品有花费和价值,这个价值是可以扩大背包容量的,这些房间并不一定是要填满的,
(t)是通过枚举得来的,因为不知道当前这个房间放几个男/女人是最优的,如果放一对夫妻的话,类似于上面的转移,一样转移即可,无非就多了一个转移的状态
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int m,f,r,c,b[N],p[N],dp[N][N][N][2];
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int main() {
m = read(), f= read(),r = read(),c = read();
for (int i=1; i<=r; i++)
cin>>b[i]>>p[i];
memset(dp,127,sizeof(dp));
int temp=dp[0][0][1][1],ans=temp;
dp[0][0][0][0]=0;
for (int i=1; i<=r; i++) //第几个房间
for (int j=m; j>=0; j--)//住下了 j 个男人
for (int k=f; k>=0; k--) { // 住了 k 个女人
if (b[i]>=2 && j>=1 && k>=1) // 这个房间可以住 一对夫妻
dp[i][j][k][1]=min(dp[i-1][j-1][k-1][0]+p[i],dp[i-1][j][k][1])
dp[i][j][k][0]=min(dp[i][j][k][0],dp[i-1][j][k][0]); // 不管住不住人,这一步都能转移
for (int t=1; t<=b[i]; t++) {
if (j-t>=0) {
dp[i][j][k][0]=min(dp[i][j][k][0],dp[i-1][j-t][k][0]+p[i]);
dp[i][j][k][1]=min(dp[i][j][k][1],dp[i-1][j-t][k][1]+p[i]);
// 这一步貌似是说有 t 个男人住进这个房间
}
if (k-t>=0) {
dp[i][j][k][0]=min(dp[i][j][k][0],dp[i-1][j][k-t][0]+p[i]);
dp[i][j][k][1]=min(dp[i][j][k][1],dp[i-1][j][k-t][1]+p[i]);
// 这一步貌似是说有 t 个女人住进这个房间
}
}
}
ans=min(dp[r][m][f][0],dp[r][m][f][1]);
if (ans==temp) cout <<"Impossible"<<endl;
else cout<<ans<<endl;
system("pause");
return 0;
}
整数划分问题
暴力Dp
(dp_{i,j,sum})表示前(i)个数选了(j)个,和为(sum)的方案数是多少
正解:
直接设(dp_{i,j})表示把(i)划分成(j)个数的方案数
小结
(DP) 最值 不必但心重叠问题 ,因为重叠之后仍然是最大值
数位DP
- 记搜为常见的实现方式
- 头铁可以写递推
但是显然两只算法我都不会,自己以前写过一篇博客有兴趣的可以看一下
树形DP
与树和图的生成树有关的DP
以每棵子树为子结构命,在父节点合并,树的天然子结构,
可以借助DFS序或者BFS序来解决问题
套数据结构(树剖)
树上问题的时间复杂度要考虑均摊
设状态一般为(f_i)子树的最优价值或方案数顺便带有附加信息(k)
树上最大独立集
树的直径
设(f_i)表示(i)这个点到子树里面的最远点是多远,然后对于每一个点(u)以这个点为根的最远路径的距离,然后找到次长链加起来即可,(f_{son_i} + e_i)
DP:
(dp_{x} = max(dep_{v in son}) - dep_x)
dfs: 从一个点随便跑一遍DFS找到最远点,然后从最远点再跑一遍DFS 找到最远点长度,就是树的直径
两遍深搜实现方式:
code:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 1e4 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume;
int sec;
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
int n, d[N];
void dfs(int x, int fa) {
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
d[to] = d[x] + 1;
if (d[to] > d[sec])
sec = to;
dfs(to, x);
}
}
int main() {
n = read();
for (int i = 1, u, v; i < n; i++) {
u = read(), v = read();
add_edge(u, v), add_edge(v, u);
}
dfs(1, 0);
d[sec] = 0, dfs(sec, 0);
printf("%d", d[sec]);
return 0;
}
树形DP
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 1e4 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int n, f[N];
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume;
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
int d1[N], d2[N], d;
void dfs(int x, int fa) {
d1[x] = d2[x] = 0;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
dfs(to, x);
int t = d1[to] + 1;
if (t > d1[x])
d2[x] = d1[x], d1[x] = t;
else if (t > d2[x])
d2[x] = t;
}
d = max(d, d1[x] + d2[x]);
}
int main() {
n = read();
for (int i = 1, u, v; i < n; i++) {
u = read(), v = read();
add_edge(u, v), add_edge(v, u);
}
dfs(1, 0);
printf("%d", d);
return 0;
}
算是个神仙题目? 但是本质很很清晰在树剖上做Dp, 但貌似本身是一个题套题
咕了,懒得写
- 重点是如何建树,因为是二叉树考虑父亲儿子表示法,以前没有写过挺……奇怪的挺奇技淫巧的
- (f_{i,0/1/2})分别表示当前点涂为红蓝绿,绿色节点的最大值,(f_{i,0/1/2})为最小值,直接根据小限制进行记忆化搜索,左右儿子分别遍历即可
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define int long long
using namespace std;
const int N = 5e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
char s[N];
int len, rt, cnt, ch[N][2];
int opt, root;
void build(int& x) {
x = ++cnt;
opt = s[cnt] - '0';
if (opt == 0)
return;
if (opt == 1)
build(ch[x][0]);
if (opt == 2)
build(ch[x][0]), build(ch[x][1]);
}
int f[N][3], g[N][3];
void dfs(int x) {
if (x == 0)
return;
int l = ch[x][0], r = ch[x][1];
dfs(l), dfs(r);
f[x][0] = max(f[l][1] + f[r][2], f[l][2] + f[r][1]) + 1;
f[x][1] = max(f[l][0] + f[r][2], f[l][2] + f[r][0]);
f[x][2] = max(f[l][0] + f[r][1], f[l][1] + f[r][0]);
g[x][0] = min(g[l][1] + g[r][2], g[l][2] + g[r][1]) + 1;
g[x][1] = min(g[l][0] + g[r][2], g[l][2] + g[r][0]);
g[x][2] = min(g[l][0] + g[r][1], g[l][1] + g[r][0]);
return;
}
signed main() {
g[1][0] = g[1][1] = 0;
g[1][2] = 1;
f[1][2] = 1;
scanf("%s", s + 1);
build(root);
dfs(1);
printf("%lld %lld", max(f[1][0], max(f[1][1], f[1][2])),
min(g[1][0], min(g[1][1], g[1][2])));
system("pause");
return 0;
}
树上背包
(f_{i,j} = max{i,j-1} + f_{son,k} | k in {1……j-1})
换根法
基环树
基环树是一种特殊的树,树最明显的特点是 (n) 个点 ( ext{n-1}) 条边,而基环树,就是 (n) 个点 (n) 条边,显然会出现环,基环树就是这样
以没有上司的舞会为原型的基环树DP
首先考虑为什么会图会变成基环树,有( ext{n})个点并且有( ext{n})条边,
树是有( ext{n})个点( ext{n-1})条边,那给树加一条边,一定会出现一个环,所以题目给出的一定是一棵基环树
假如说有这样一个基环树,我们首先断环成链,我们断开 1 和 2 这两个点之间的连边,于是我们分别以 1 号和 2 号节点为根,形成了这样两棵树
然后我们就开始跑DP 就是没有上司的舞会那个很简单的DP,然后我们考虑如何处理环,我们就对每次断开的 (f_{root1,0}) 和 (f_{root2,0}) 取 (max)
为什么两次都是对这个点不选去(max)?理由很简单
- 当取(f_{root1,0})时,我们( ext{root2} ext{root1})为根的树中是可选可不选的,同理当取(f_{root2,0})时,我们( ext{root1} ext{root2})为根的树中是可选可不选的,已经包含了两个点都不选和两个点选一个的情况,所以答案就是每次都断开环上的一条边,然后取以(root1)和(root2)为根的当前点不选的最大值,然后求和即可
for (int i = 1; i <= n; i++) {
if (vis[i])
continue;
dfs(i, 0);
dp(root1, 0);
int ans_ = f[root1][0];
dp(root2, 0);
ans += max(ans_, f[root2][0]);
}
这个地方千万不能写成
for (int i = 1; i <= n; i++) {
if (vis[i])
continue;
dfs(i, 0);
dp(root1, 0);
dp(root2, 0);
ans += max(f[root1][0], f[root2][0]);
}
因为以(root2)为根再跑一遍DP后(f_{root1,0})的值会改变只有我这个sb没有意识到这个问题调了半天
/*
Author:Imy_isLy
知识点:基环树Dp
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define int long long
using namespace std;
const int N = 1e6 + 100;
//=============================================================
int n, val[N];
int del, f[N][2], root1, root2;
bool vis[N];
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume = 1, ans;
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
void dp(int x, int fa) {
f[x][1] = val[x], f[x][0] = 0;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
if (i == del || i == (del ^ 1))
continue;
dp(to, x);
f[x][0] += max(f[to][0], f[to][1]);
f[x][1] += f[to][0];
}
}
void dfs(int x, int fa) {
v
is[x] = true;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
if (vis[to]) {
del = i;
root1 = to, root2 = x;
continue;
}
dfs(to, x);
}
}
signed main() {
n = read();
for (int i = 1; i <= n; i++) {
val[i] = read();
int x = read();
add_edge(i, x), add_edge(x, i);
}
for (int i = 1; i <= n; i++) {
if (vis[i])
continue;
dfs(i, 0);
dp(root1, 0);
int ans_ = f[root1][0];
dp(root2, 0);
ans += max(ans_, f[root2][0]);
}
printf("%lld", ans);
system("pause");
return 0;
}
和上面一样的最大点独立集问题,只不过因为数据的原因,上一个题可以是基环树森林,而这个题只有一颗基环树,那就稍微改一下求和就可以了
不改也能过
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 1e6 + 100;
//=============================================================
int n, val[N];
double K;
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume = 1;
int root1, root2, del, ans;
int f[N][2];
bool vis[N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
void dp(int x, int fa) {
f[x][0] = 0, f[x][1] = val[x];
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
if (i == del || i == (del ^ 1)) continue;
dp(to, x);
f[x][0] += max(f[to][0], f[to][1]);
f[x][1] += f[to][0];
}
}
void dfs(int x, int fa) {
vis[x] = 1;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
if (vis[to]) {
del = i;
root1 = to, root2 = x;
continue;
}
dfs(to, x);
}
}
int main() {
n = read();
for (int i = 1; i <= n; i++)
val[i] = read();
for (int i = 1, u, v; i <= n; i++) {
u = read() + 1, v = read() + 1;
add_edge(u, v), add_edge(v, u);
}
scanf("%lf", &K);
for (int i = 1; i <= n; i++) {
if (vis[i]) continue;
dfs(i, i);
dp(root1, root1);
int ans_ = f[root1][0];
dp(root2, root2);
ans = max(ans_, f[root2][0]);
}
printf("%.1lf", K * ans);
system("pause");
return 0;
}
我是sb我要回去重新学单调队列优化Dp了……
首先,题目要求是基环树森林的最长直径,思路不是很难想,但是很难写,我写到一半就傻了
先说暴力,首先断环为链,然后老规矩,以这两个点为根,跑树形DP,求出树的最长链,然后这个复杂度大概是( ext{O(环长 + n^2)})的……这很显然……会T到飞……
考虑优化:
首先在基环树上找到环,然后在环上搞一个前缀和(s_i),环的总长度为(sum),以(i)为根的最长链为(d_i)
当&i&一定时,(s_i + d_i,sum - s_i + d_i)是一个定值,那就维护一个(j)就好了即(s_j - d_j)最小(s_j + d_j) 最大即可
#include <cstdio>
#include <iostream>
#include <queue>
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6 + 100;
ll n, nume = 0, cnt, ans1, ans, st, ans2, ans3;
ll dis[2 * N], head[N], to[2 * N], net[2 * N];
ll vis[N], vis2[N], id[N], d[N], dp[2 * N], s[N];
void add(int x, int y, int z) {
dis[++nume] = z, to[nume] = y, net[nume] = head[x], head[x] = nume;
}
bool dfs(int now, int la) {
if (vis[now] == 1) {
vis[now] = 2, id[++cnt] = now, vis2[now] = 1;
return 1;
}
vis[now] = 1;
for (int i = head[now]; i; i = net[i])
if (i != ((la - 1) ^ 1) + 1 && dfs(to[i], i)) {
if (vis[now] != 2)
id[++cnt] = now, vis2[now] = 1, s[cnt] = s[cnt - 1] + dis[i];
else {
s[st - 1] = s[st] - dis[i];
return 0;
}
return 1;
}
return 0;
}
void tree_dp(int now) {
vis2[now] = 1;
for (int i = head[now]; i; i = net[i]) {
int y = to[i];
if (vis2[y])
continue;
tree_dp(y);
ans1 = max(ans1, d[now] + d[y] + dis[i]);
d[now] = max(d[now], d[y] + dis[i]);
}
}
ll brt(int root) {
st = cnt + 1, ans2 = 0, ans3 = 0;
dfs(root, 0);
for (int i = st; i <= cnt; i++) {
ans1 = 0;
tree_dp(id[i]);
ans2 = max(ans2, ans1);
dp[i + cnt - st + 1] = dp[i] = d[id[i]];
s[i + cnt - st + 1] = s[i + cnt - st] + s[i] - s[i - 1];
}
deque<int> q;
for (int i = st; i <= 2 * cnt - st + 1; i++) {
while (q.size() && q.front() <= i - cnt + st - 1)
q.pop_front();
if (q.size())
ans3 = max(ans3, dp[i] + dp[q.front()] + s[i] - s[q.front()]);
while (q.size() && dp[q.back()] - s[q.back()] <= dp[i] - s[i])
q.pop_back();
q.push_back(i);
}
return max(ans2, ans3);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
int y, z;
scanf("%d %d", &y, &z);
add(i, y, z);
add(y, i, z);
}
for (int i = 1; i <= n; i++)
if (!vis2[i])
ans += brt(i);
cout << ans;
system("pause");
return 0;
}
DP 优化
加速状态转
- 前缀和优化
- 数据结构优化
单调队列优化
- 一般情况下可以把原来的DP 式子进行简化变成(dp_[i] = max(f_j) +g_i) ((g_i)与(j)是无关变量)并且(j)的取值是一段连续的区间,具有单调性
设状态: (f_{i,j}) 表示第( ext{i})天,有( ext{j})张股票的最大收益
考虑分类讨论,因为存在买与不买,卖与不卖这四种情况
- 凭空买:
直接附初值 (f_{i,j} = { - j * ap | j <= maxP })
- 不买也不买:
反正不买也不卖,持有的股票数是不变的,那就从前往后递推就好了 (f_{i,j} = max{f_{i-1,j}})
- 我要继续追加(继续买
因为两次交易要相隔( ext{W})天,设当前第( ext{x})天,拥有的股票为(k),那上一次交易最近是在第( ext{x - W - 1})天,万一这一天没有发生交易,可以通过情况二,把之前的转移到(f_{x - W - 1, k})
(f_{i,j} = max{f_{i - w - 1, k} - (j - k) imes ap | j - as<= k < j })
- 我要卖!
同样要相隔( ext{W})天,设当前是第( ext{x})天,拥有的股票为( ext{k}),那上一次交易最近是在第( ext{x - W - 1})天,万一这一天没有发生交易,也可以通过情况二,把之前的转移到(f_{x - W - 1, k}) ,把上面的转移稍微改一下就好
(f_{i,j} = max{f_{i - w - 1, k} + (k - j) imes bp | j < k <= j + bs })
-
把3,4两种情况的转移方程化简一下,就变成了这样一个东西
-
3:(f_{i,j} = max{f_{i - w - 1, k}+ k imes ap} - j imes ap)(~~~~)限制: (j - as<= k < j)
-
4:(f_{i,j} = max{f_{i - w - 1, k}+ k imes bp} - j imes bp)(~~~~)限制: (j < k <= j + bs)
然后发现在股票相同的同一层内,max外面的是一个常数,问题就变成了(K=?)时能取到最大值,(k)属于满足转移条件的一个区间,然后单调队列优化就好了,单调队列里存的就是那个(K)
买的时候,是顺着买的,正序枚举莫得问题,卖的时候,需要倒叙枚举,因为由一个股票多的状态转移到股票少的状态;
买:比如当bs等于3的时候,我们枚举j到2时,我们股票为3 4 5时可以卖出。所以我们可以选择的区间是3~5.可以正序枚举的原因是在情况2时我们把最优解向后转移了
卖:同理,我们的每卖掉股票时, 可以卖成 x - bs(3)的数。数字越小,对后面的数可以操作的越多。倒序枚举就可以维护单调最优
……浪费了半天,意识到,我们第 4 个是由股票多的情况向股票少的情况进行转移,正序枚举显然不满足这个条件很显然,但我思考了半天因为如果正序枚举,在单调队列中的(k)确实是满足单调性的,但是(K)不是从比(j)大的地方转移到(j)这个(K)是比(j)小的
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 2010;
//=============================================================
int n, maxP, W;
int ap, bp, as, bs, f[N][N];
int q[N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
//=============================================================
int main() {
n = read(), maxP = read(), W = read();
memset(f, 128, sizeof(f));
for (int i = 1; i <= n; i++) {
ap = read(), bp = read(), as = read(), bs = read();
for (int j = 0; j <= as; ++j)
f[i][j] = -j * ap;
for (int j = 0; j <= maxP; ++j)
f[i][j] = max(f[i][j], f[i - 1][j]);
if (i <= W) continue;
int head = 1, tail = 0;
for (int j = 0; j <= maxP; ++j) {
while (head <= tail && q[head] < j - as)
head++;
while (head <= tail && f[i - W - 1][q[tail]] + q[tail] * ap <= f[i - W - 1][j] + j * ap)
tail--;
q[++tail] = j;
if (head <= tail)
f[i][j] = max(f[i][j], f[i - W - 1][q[head]] + q[head] * ap - j * ap);
}
head = 1, tail = 0;
for (int j = maxP; j >= 0; --j) {
while (head <= tail && q[head] > j + bs)
head++;
while (head <= tail && f[i - W - 1][q[tail]] + q[tail] * bp <= f[i - W - 1][j] + j * bp)
tail--;
q[++tail] = j;
if (head <= tail)
f[i][j] = max(f[i][j], f[i - W - 1][q[head]] + q[head] * bp - j * bp);
}
}
int ans = -2147483647;
for (int i = 0; i <= maxP; i++)
ans = max(ans, f[n][i]);
printf("%d", ans);
system("pause");
return 0;
}
CF372C Watching Fireworks is Fun
(f_{i,j})表示看第( ext{i})个烟花时,位置为( ext{j})的最大快乐值,
(f_{i,j} = max{f_{i-1,k} + b_i - |a_i-j| })
其中要满足条件(j - (t_i-t_{i-1}) imes d <= k <= j + (t_i - t_{i - 1}) imes d))
如果(i和j)是确定的,那么(~~~~- |a_i-j| + b_i~~~~) 也是一个定值,可以从( ext{max})里面提出来,那原式就变成了
(f_{i,j} = max{f_{i-1,k}} - |a_i-j| + b_i)
按常理说这样就可以了,但是CF的题它不可能这么简单……,它会MLE,可以发现,(i)这一维只会从(i-1)这一维转移过来,可以用滚动数组,把第一维给压掉
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define int long long
using namespace std;
const int N = 1.5e5+10;
const int M = 310;
//=============================================================
int f[2][N], n, m, d, ans = -1e18, sum;
int a[M], b[M], t[M], q[N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
//=============================================================
signed main() {
n = read() , m = read() , d = read();
for (int i = 1 ; i <= m ; i++) {
a[i] = read() , b[i] = read() , t[i] = read();
}
memset(f,128,sizeof(f));
for (int i = 1 ; i <= n ; i++) f[0][i] = 0;
int fl = 1;
for (int i = 1 ; i <= m ; i++) {
int head = 1 , tail = 0 , k = 1;
for(int j = 1 ; j <= n ; j++) {
while(k <= min(n,j + d * (t[i] - t[i - 1]))) {
while(head <= tail && f[fl ^ 1][q[tail]] <= f[fl^1][k]) tail--;
q[++tail] = k;
k++;
}
while(head <= tail && q[head] < max(1ll,j - d * (t[i] - t[i - 1]))) head++;
f[fl][j] = f[fl ^ 1][q[head]] + b[i] - abs(a[i] - j);
}
fl ^= 1;
}
for (int i = 1 ; i <= n ; i++) ans = max(ans,f[fl ^ 1][i]);
printf("%lld",ans);
system("pause");
return 0;
}
前缀和&单调队列优化转移的合法点都是一整个区间
树状数组&线段树优化
(dp_i =max{dp_j~~|~~a[j] < a[i] &&j < i} + 1)
权值线段树或树状数组优化
二分优化
状压DP
- 数据范围一维较正常一维很小,较容易识别
状压DP入门题, 设(f_{i,j,s})表示第(i)行放棋子的方式(s),已经放了(s)个棋子
(f_{i,j,s} += f_{i - 1 , j - cnt(s),T})
转移条件 :(((T<<1|T>>1)&S) == 0)
--------------------------------------by zhx
那么问题就转化了,转化为了如果何找到(T)会发现,(T)中1的个数完全可以通过预处处理出来,到时候如果($)满足条件,可以通过预处理出来的数组(sit_x)进行统计答案
我不太适应上面的转移方程,我还是习惯Kersen 的做法
(dp_{i,j,cnt}) 表示第(i)行的排列方式为 (j) 放了(cnt)个棋子
用(sit_x)表示哦(x)中(1)的个数,记当前放置棋子为(gs)
(f_{i,j,s} = sum f_{i - 1,k,s - gs_j})
转移条件:
(sit_j & sit_k == 0)
(((sit_j << 1) & sit_k) == 0)
((sit_j & (sit_k << 1) )== 0)
code:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 120;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int s0;
ll f[20][120][120],num[120],s[120];
int main() {
int n = read(), k = read();
for (int i = 0; i < (1 << n); i++) {
if (i & (i << 1))
continue;
int kk = 0;
for (int j = 0; j < n; j++)
if (i & (1 << j))
kk++;
s[++s0] = i, num[s0] = kk;
}
f[0][1][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= s0; j++) {
for (int k_ = num[j]; k_ <= k; k_++) {
for (int t = 1; t <= s0; t++) {
if (!(s[t] & s[j]) && !(s[t] & (s[j] >> 1)) && !(s[t] & (s[j] << 1)))
f[i][j][k_] += f[i - 1][t][k_ - num[j]];
}
}
}
}
ll ans = 0;
for (int i = 1; i <= s0; i++)
ans += f[n][i][k];
printf("%lld", ans);
system("pause");
return 0;
}
枚举子集的DP:
-
状压DP枚举子集的一种思想
-
首先发现输入的(m)对于答案是没有任何影响的,是个混淆的东西
-
两个点确定一条二次函数,可以(n^3)预处理一下,看看这条二次函数能经过多少点
-
(f[i|num[j][k]]=min(f[i|num[j][k]],f[i]+1) | nump[j][k])为预处理出来的数组,(f_{s})是子集(s)的最小需求
-
然后转移就可以了
-
注意一个优化,要不然会TLE ,我们从前一直往后打就了可以了,找到一个可以打到的,就直接break因为后面如果还能达到,会被重新覆盖掉
-
虽然这么说不大好,因为DP要求的是局部的最优解,从而引出整体最优解, 如果不考虑后面,确实当前已经是最优解了,但是后面如果还能打到前面的,纯粹就是捎带的
夹带私活,把前面的活抢了所以可以直接转移
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = (1 << 19);
const double esp = 1e-6;
const int inf = 0x3f3f3f3f;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
double x[20], y[20];
int num[20][20], f[N];
int main() {
int T = read();
while (T--) {
int n = read(), m = read();
for (int i = 1; i <= n; i++)
scanf("%lf%lf", &x[i], &y[i]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
num[i][j] = 0;
for (int i = 1; i < n; i++) {
for (int j = i + 1; j <= n; j++) {
if (fabs(x[i] - x[j]) < esp)
continue;
double a = (y[j] / x[j] - y[i] / x[i]) / (x[j] - x[i]);
if (a >= 0)
continue;
double b = y[i] / x[i] - a * x[i];
for (int k = 1; k <= n; k++) {
if (fabs(a * x[k] + b - y[k] / x[k]) < esp) {
num[i][j] |= (1 << (k - 1));
}
}
}
}
for (int i = 0; i <= (1 << n) - 1; i++)
f[i] = inf;
f[0] = 0;
for (int i = 0; i <= (1 << n) - 1; i++) {
for (int j = 1; j <= n; j++) {
if (!(i & (1 << (j - 1)))) {
for (int k = j; k <= n; k++) {
if (j == k)
f[i | (1 << (j - 1))] = min(f[i | (1 << (j - 1))], f[i] + 1);
if (fabs(x[j] - x[k]) < esp)
continue;
f[i | num[j][k]] = min(f[i | num[j][k]], f[i] + 1);
}
break;
}
}
}
printf("%d
", f[(1 << n) - 1]);
}
system("pause");
return 0;
}
斜率优化DP
要求输出(N)个数字的(a_i),输出的时候可以连续的输出,每一段的代价为这一段的平方和加上一个常数(M)
(dp_i = min{dp_j + (s_{i + 1} - s_j)^2 + M | j < i})
斜率具有单调性,可以,类似于单调队列的思想,如果后者的比前者优,那前者可以删,
单调队列进行维护即可
([l,r])区间内的代价为((r - l + sumlimits_{i=l}^r c_i-L)^2)
(sumlimits_{i=l}^r c_i)可以通过前缀和处理出来,等于(sum_r - sum_{l-1})
原式等于(r - l + sum_r - sum_{l-1} - L)
然后就可以得到一个(n^2)的转移方程
令(f_i)表示前(i)个数的和
(f_i = min_{j<i}{f_j + (sum_i - sum_{j} + i - j - 1- L)^2})
将式子进一步处理
- 令(s_i = sum_i + i)
- (L^{'} = L +1)
(f_{i} = min{f_j + (s_i - s_j - L^{'})})
将含有(j)的都移到同一边
(f_{i} - (s_i -L^{'})^2 = min{f_j + s_j^2 + 2 imes s_j(L^{'} - s_i)})
设函数:
转移方程变成了(b_i = min_{j < i}{y_j-k_ix_j})
把((x_i,y_i))看成二维平面上的点,则(k_i)表示直线斜率,(b_i)表示过((x_i.y_i))的斜率为(k_i)的直线 截距,那么问题就转化为了选择合适的(j)然后最小化截距,因为上面的式子中,(b_i)的等式后面中((s_i - L')^2)是一个定值,所以最小化截距就是最小化(f_i)
图片摘自Oi-Wiki
然后下凸包的函数跑斜率优化就OK
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#define LL long long
#define double long double
const int kN = 5e4 + 10;
using namespace std;
//=============================================================
int n, head = 1, tail, q[kN];
LL L, s[kN], f[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-')
f = -1;
for (; isdigit(ch); ch = getchar())
w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
double getk(int x, int y) {
if (s[x] == s[y])
return (f[y] + s[y] * s[y] > f[x] + s[x] * s[x] ? 1e18 : -1e18);
return (double)(((f[y] + s[y] * s[y]) - (f[x] + s[x] * s[x])) / (s[y] - s[x]));
}
int Query(int now_) {
while (head < tail && getk(q[head], q[head + 1]) <= (double)(-2 * (L - s[now_])))
++head;
return q[head];
}
void Insert(int now_) {
while (head < tail && getk(q[tail - 1], q[tail]) >= getk(q[tail - 1], now_))
--tail;
q[++tail] = now_;
}
//=============================================================
int main() {
n = read(), L = read() + 1;
Insert(0);
for (int i = 1, j = 0; i <= n; ++i) {
s[i] = s[i - 1] + read() + 1;
j = Query(i);
f[i] = f[j] + 1ll * (s[i] - s[j] - L) * (s[i] - s[j] - L);
// cout << "j: " << j << "
";
Insert(i);
}
printf("%lld
", f[n]);
system("pause");
return 0;
}
P2900 [USACO08MAR]Land Acquisition G
因为数据是无序的所以,我们首先对数据进行排序,以(x)为第一优先级降序排序,以(y)为第二优先级从小到大排序,然后发现,第二维并非具有绝对的单调性,那把这一段处理一下,让其具有单调性即可
bool cmp(node x, node y) {
return x.x != y.x ? x.x > y.x : x.y < y.y;
}
for (int i = 1; i <= n; i++)
a[i] = (node){read(), read()};
sort(a + 1, a + 1 + n, cmp);
int temp = 0;
for (int i = 1; i <= n; i++) {
if (a[i].y > a[temp].y)
a[++temp] = a[i];
}
n = temp;
对于一段区间([l,r])其代价为(x_l imes y_r)
设(f_i)表示按这种顺序划分后,前(i)片土地的最小代价和,然后枚举上一段的结尾位置
(f_i = min_{j = 0}^{i-1} {f_j + x_{j+1} imes y_i})
很明显的(n^2)转移,考虑优化,因为存在乘积的形式,所以考虑斜率优化,设:
那问题就又变成了最小化截距的问题了,用单调队列维护下凸包,然后转移即可
/*
Author:Imy_isLy
知识点:
*/
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 5e4+100;
//=============================================================
int n;
struct node {
ll x, y;
} a[N];
ll f[N];
int q[N], head = 1, tail = 0;
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
bool cmp(node x, node y) {
return x.x != y.x ? x.x > y.x : x.y < y.y;
}
double getk(int x, int y) {
return (double)(f[y] - f[x]) / (-a[y + 1].x + a[x + 1].x);
}
void insert(int now) {
while (head < tail && getk(q[tail - 1], q[tail]) >= getk(q[tail - 1], now))
tail--;
q[++tail] = now;
}
int query(int now) {
while (head < tail && getk(q[head], q[head + 1]) <= a[now].y)
head++;
return q[head];
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; i++)
a[i] = (node){read(), read()};
sort(a + 1, a + 1 + n, cmp);
int temp = 0;
for (int i = 1; i <= n; i++) {
if (a[i].y > a[temp].y)
a[++temp] = a[i];
}
n = temp;
insert(0);
for (int i = 1, j = 0; i <= n; i++) {
j = query(i);
f[i] = f[j] + 1ll * a[j + 1].x * a[i].y;
insert(i);
}
printf("%lld",f[n]);
system("pause");
return 0;
}
记化成的(m)段,每一段的和为(x_i)
方差的公式转化:
原式要求的是(m^2cdot S^2)
通过上面的变形,可以直接得到
假如说原来的数字为(a_?)
然后发现((moverline{x})^2)这一块就是所有数字和的平方等价于((sumlimits_{i=1}^na_i)^2)把原式进行处理变成一样的形式变成了((sumlimits_{i=1}^mb_i)^2)
设(f_{i,k})表示前(i)个数分成(k)段(b_i)
记一个前缀和(sum)
将(i)移到等号的同一边
进行转化
然后就可以得出(b_i = y_j -k_ix_j),当(i)一定时(k_i,sum_i)是定值,然后问题就又变成了最小化截距问题了
这个题在一些很细节的地方卡精度
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
#define double long double
using namespace std;
const int N = 3010;
//=============================================================
int n, m, a[N];
ll f[N][N], sum[N];
int q[N], head = 1, tail = 0;
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
double getk(int id, int x, int y) {
if (sum[x] == sum[y])
return f[y][id] + sum[y] * sum[y] > f[id][x] + sum[x] * sum[x] ? 1e18
: -1e18;
return (double)(((double)(f[y][id] + sum[y] * sum[y]) -
(double)(f[x][id] + sum[x] * sum[x])) /
(sum[y] - sum[x]));
}
void insert(int id, int now_) {
while (head < tail &&
getk(id, q[tail - 1], q[tail]) >= getk(id, q[tail - 1], now_))
--tail;
q[++tail] = now_;
}
int query(int id, int now_) {
while (head < tail &&
getk(id, q[head], q[head + 1]) <= (double)(2 * sum[now_]))
head++;
return q[head];
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
sum[i] = sum[i - 1] + a[i];
}
memset(f, 63, sizeof(f));
f[0][0] = 0;
for (int k = 1; k <= m; ++k) {
head = 1, tail = 0;
insert(k - 1, 0);
for (int i = 1; i <= n; i++) {
if (i >= k) {
int j = query(k - 1, i);
f[i][k] = f[j][k - 1] + (sum[i] - sum[j]) * (sum[i] - sum[j]);
}
insert(k - 1, i);
}
}
printf("%lld", 1ll * m * f[n][m] - 1ll * sum[n] * sum[n]);
system("pause");
return 0;
}
任务安排
斜率优化的套路题,上面几个题虽然很难,但是不够恶心,会发现,当一个物品的属性变成两个之后,就想让自己多长几个脑子
给定n个物品,每一个物品都有 ((t,g))两个属性,给定一个常数(s)将物品进行分段,第(i)段区间([l,r])的价值为((is + sumlimits_{j= l}^rt_j) imes sumlimits_{k = l}^rg_i)
建议看这篇博客学习?
先扯一句 :
求斜率我用X 除以 Y 算咋回事
solution:
设 (f_i) 表示 前 (i)个人的最大战力
(f_i = f_j + a imes (sum_i - sum_j)^2 + b imes (sum_i - sum_j) + c)
设存在 (j < k~~&& ~~ f_j > f_k)
(f_j + a imes (sum_i - sum_j)^2 + b imes (sum_i - sum_j) + c > \ f_k + a imes (sum_i - sum_k)^2 + b imes (sum_i - sum_k) + c)
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define int long long
using namespace std;
const int N = 1e6 + 100;
const int inf = 0x3f3f3f3f;
//=============================================================
int f[N], sum[N]; // 表示前 i 个人分成?组的最大价值
int q[N], head = 1, tail = 0;
int a, b, c, n;
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int X(int x) {
return sum[x];
}
int Y(int x) {
return f[x] + a * sum[x] * sum[x] - b * sum[x];
}
double get_K(int x, int y) {
return (Y(x) - Y(y)) / (X(x) - X(y));
}
void Insert(int now_) {
while (head < tail && get_K(q[tail - 1], q[tail]) < get_K(q[tail - 1], now_))
--tail;
q[++tail] = now_;
}
int query(int now_) {
while (head < tail && get_K(q[head], q[head + 1]) > 2 * a * sum[now_])
head++;
return q[head];
}
signed main() {
n = read(), a = read(), b = read(), c = read();
Insert(0);
for (int i = 1; i <= n; ++i) {
sum[i] = sum[i - 1] + read();
int j = query(i);
// cout << j <<"
";
f[i] = f[j] + a * (sum[i] - sum[j]) * (sum[i] - sum[j]) + b * (sum[i] - sum[j]) + c;
Insert(i);
}
cout << f[n];
system("pause");
return 0;
}
数学期望与期望DP
期望:
(e(x) = val_x imes P(x))
(e(x + y) = e(x) + e(y))
如果两个事件互为独立事件则
(e(x imes y) = e(x) imes e(y))
概率期望 ( ext{DP})
因为最终要求的期望与边权挂钩,而且要求最后期望值最小,可发现点数为300.,可以莽一个( ext{Floyed})求一个全源最短路
设(f_{i,j,0/1})表示前(i)个课程申请了(j)次,且第(i)个是否申请时的最小期望值
(C1=c[i−1],C2=d[i−1],C3=c[i],C4=d[i])
我写不明白了~
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 2e3 + 5;
const double inf = 1e17 + 5;
int n, m, v, e, c[N][2], dis[305][305];
double k[N], dp[N][N][2], ans;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int main() {
n = read(), m = read(), v = read(), e = read();
for (int i = 1; i <= n; i++) c[i][0] = read();
for (int i = 1; i <= n; i++) c[i][1] = read();
for (int i = 1; i <= n; i++) scanf("%lf", &k[i]);
for (int i = 1; i <= v; i++)
for (int j = 1; j <= v; j++)
dis[i][j] = 1e9, dis[i][i] = dis[i][0] = dis[0][i] = 0;
for (int i = 1; i <= e; i++) {
int x = read(), y = read(), w = read();
dis[x][y] = dis[y][x] = min(dis[x][y], w);
}
for (int k = 1; k <= v; k++)
for (int i = 1; i <= v; i++)
for (int j = 1; j <= v; j++)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
for (int i = 0; i <= n; i++)
for (int j = 0; j <= m; j++)
dp[i][j][0] = dp[i][j][1] = inf;
dp[1][0][0] = dp[1][1][1] = 0;
for (int i = 2; i <= n; i++) {
dp[i][0][0] = dp[i - 1][0][0] + dis[c[i - 1][0]][c[i][0]];
for (int j = 1; j <= min(i, m); j++) {
int C1 = c[i - 1][0], C2 = c[i - 1][1], C3 = c[i][0], C4 = c[i][1];
dp[i][j][0] =
min(dp[i][j][0],
min(dp[i - 1][j][0] + dis[C1][C3],
dp[i - 1][j][1]
+ dis[C1][C3] * (1 - k[i - 1])
+ dis[C2][C3] * k[i - 1]));
dp[i][j][1] =
min(dp[i][j][1],
min(dp[i - 1][j - 1][0]
+ dis[C1][C3] * (1 - k[i])
+ dis[C1][C4] * k[i],
dp[i - 1][j - 1][1]
+ dis[C2][C4] * k[i] * k[i - 1]
+ dis[C2][C3] * k[i - 1] * (1 - k[i])
+ dis[C1][C4] * (1 - k[i - 1]) * k[i]
+ dis[C1][C3] * (1 - k[i - 1]) * (1 - k[i])));
}
}
ans = inf;
for (int i = 0; i <= m; i++)
ans = min(ans, min(dp[n][i][0], dp[n][i][1]));
printf("%.2lf", ans);
system("pause");
return 0;
}
因当前的宝物能不能拿有限制,所以把限制给状压了 ((j & sta[i]) == sta[i])就满足了限制
转移方程:
下面的代码是倒序枚举的(i)所以是(i+1)
/*
Author:Imy_isLy
知识点:状压DP
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int K, n, p[20], sta[1 << 15];
double f[105][1 << 15];
int main() {
K = read(), n = read();
for (int i = 1, x; i <= n; i++) {
p[i] = read();
while (x = read()) {
sta[i] = sta[i] | (1 << x - 1);
}
}
for (int i = K; i; --i) {
for (int j = 0; j < (1 << n); j++) {
for (int k = 1; k <= n; k++) {
if ((j & sta[k]) == sta[k])
f[i][j] += max(f[i + 1][j], f[i + 1][j | (1 << k - 1)] + p[k]);
else
f[i][j] += f[i + 1][j];
}
f[i][j] /= n;
}
}
printf("%.6lf", f[1][0]);
system("pause");
return 0;
}
貌似和DP没有啥关系……就是纯粹求个数学期望
(p_{i,j})表示结束位置为 (i) 的连续(1)串的,其长度为(j)的概率
((x + 1) ^ 3)
(Longrightarrow x^3 + 3 imes x^2 + 3 imes x + 1)
可以发现式子中多了这么一部分(3 imes x^2 + 3 imes x + 1)
可以维护一个(x^2)的期望 和 (x) 的期望
(x : l1[i] = (l1[i - 1] + 1) * p_i)
(x^2 :l2[i] = (l2[i - 1] + 2 imes l1[i-1] + 1) imes p_i)
(ans[i] = ans[i - 1] + (3 imes l2[i-1] + 3 imes l1[i-1]) imes p[i])
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 1e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
double l[N], l2[N];
int main() {
int n = read();
double ans = 0;
for (int i = 1; i <= n; i++) {
double x;
scanf("%lf", &x);
l[i] = (l[i - 1] + 1) * x;
l2[i] = (l2[i - 1] + 2 * l[i - 1] + 1) * x;
ans += (3 * l2[i - 1] + 3 * l[i - 1] + 1) * x;
}
printf("%.1lf", ans);
system("pause");
return 0;
}
没学会的题:
- 梦幻岛宝珠 分层DP?
upd:
2021.4.14
今天退役啦,hhhh滚回去卷文化课啦