首先 这是一道图论题
读题很重要!!!
读完题后 我们会发现 这是一张有向图 并且是一个“横向”的图 题目要求只能从西向东走,是一个DAG,我们只需要进行一次拓扑排序(topsort) 然后用拓扑序进行DP就好了
拓扑排序:对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
拓扑排序的方法:我们只需要在读入时将被指向的点入度 +1 在 top sort 时首先将入度为 0 的点入队,枚举它的每一条边,这些边指向的点的入度 -1,当某一个点入度为 0 时,让他进队就好了。
A
−
>
B
−
>
C
A->B->C
A−>B−>C
A影响B(A可以到达B),B影响C,则A影响C,但C一定不会影响B。这就满足了DP的无后效性,也就是
某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响
到达 u 这一点可能有多种走法,我们就可以DP找最大值 a n s [ u ] = m a x ( a n s [ u ] , a n s [ v ] + 1 ) ans[u] = max(ans[u], ans[v] + 1) ans[u]=max(ans[u],ans[v]+1)
若一个联通图中所有点的度数都是偶数,则这个图是欧拉回路。
若一个联通图中只有 0 个点或 2 个点的度数为奇数,则这个图是欧拉路径。
这题的意思是,输出一条路径,使得图不经过重复的边。
这是一个欧拉路问题,把每两个点间加一个无向边,判断每个点的度是否为偶数(欧拉回路),或者是否有两个点是奇数(欧拉路径)。
因为要输出前面的字母的 ASCII 编码尽可能小的(字典序最小)的方案,所以找到欧拉路径的起点或回路的字典序最小点,开始 dfs。
DP
于是 … Kumii 就滚去 Menci 博客里找题做啦 … 我才不会告诉你我是倒着做的
「CodeVS 3269」混合背包 - 背包 DP
背包九讲
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示前
i
i
i个人,抄到第
j
j
j本书,抄的最多的人最少抄多少
f
[
i
]
[
j
]
=
m
i
n
(
m
a
x
(
f
[
k
−
1
]
[
j
−
1
]
,
s
u
m
[
i
]
−
s
u
m
[
k
]
)
)
f[i][j] = min(max(f[k-1][j-1], sum[i]-sum[k]))
f[i][j]=min(max(f[k−1][j−1],sum[i]−sum[k])) ,sum[] 是维护的前缀和
(
s
u
m
[
i
]
−
s
u
m
[
k
]
sum[i]-sum[k]
sum[i]−sum[k] 是第
i
i
i个人抄书的页数,与前
i
−
1
i-1
i−1个人的抄书页数最多的人比
m
a
x
max
max)
求出
f
[
n
]
[
K
−
1
]
f[n][K-1]
f[n][K−1]即
n
n
n本,分给
K
K
K个人抄,抄的最多的人最少抄多少
题目要求靠前的人尽可能抄的少
那么从后往前扫一遍,从后往前分成一个个区间,每个区间不超过
f
[
n
]
[
K
−
1
]
f[n][K-1]
f[n][K−1]就行
区间 DP
我们发现,中序遍历区间内任何一点都可以当做根,根左边的部分一定是左子树,根右边的一定是右子树。
我们可以区间 DP 枚举根,然后开个数组记录根。先序遍历时就按照根的顺序递归二分输出根就好了。
注意 d p dp dp 的时候 i i i 是倒序的,因为如果正序的话第一次循环 i i i 是 1 后面 j = n j = n j=n 了,这样 f [ i ] [ j ] f[i][j] f[i][j] 不就已经转移出来了,那你 i = 2 , i = 3 … i = 2, i = 3 … i=2,i=3… 的转移有什么用,所以显然不对。
#include <bits/stdc++.h>
using namespace std;
const int N = 35;
int n;
int root[N][N];
long long f[N][N];
void dp() {
for(int i = n - 1; i >= 1; i --) //倒序!
for(int j = i + 1; j <= n; j ++)
for(int k = i; k <= j; k ++) {
if(i + 1 == j){
f[i][j] = f[i][i] + f[j][j];
root[i][j] = i;
}
else if(f[i][k - 1] * f[k + 1][j] + f[k][k] > f[i][j]) {
f[i][j] = f[i][k - 1] * f[k + 1][j] + f[k][k];
root[i][j] = k; // 注意只有当满足条件以后再设跟为 k
}
}
}
void dfs(int l, int r) {
if(l == r){
printf("%d ", l); return;
}
if(l > r)return;
int x = root[l][r];
printf("%d ", x);
dfs(l, x - 1);
dfs(x + 1, r);
return ;
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i ++)
scanf("%lld", &f[i][i]);
dp();
printf("%lld\n", f[1][n]);
dfs(1, n);
return 0;
}
线性 DP
Luogu P1564 膜拜 / 小 ly 的题解 —— 膜拜 小 ly)))
记膜拜神牛甲的人为 1,膜拜神牛乙的人为 -1。
题目就变成了将序列最少分为多少段,使每段和的绝对值
≤
M
≤ M
≤M 或等于人数。于是我们可以用前缀和
s
u
m
[
i
]
sum[i]
sum[i] 算出前
i
i
i 个数之和。去枚举每一个
j
j
j 段即可。
设 f [ i ] f[i] f[i] 表示前 i i i 个人的最少需要机房数
f
[
i
]
=
min
(
min
{
f
[
j
]
,
a
b
s
(
s
u
m
[
i
]
−
s
u
m
[
j
−
1
]
)
=
=
i
−
j
+
1
∣
∣
a
b
s
(
s
u
m
[
i
]
−
s
u
m
[
j
−
1
]
)
<
=
m
}
+
1
,
f
[
i
]
)
f[i] = {\min}({\min}\{ f[j], abs(sum[i] - sum[j - 1]) == i - j + 1 || abs(sum[i] - sum[j - 1]) <=m \} + 1, f[i])
f[i]=min(min{f[j],abs(sum[i]−sum[j−1])==i−j+1∣∣abs(sum[i]−sum[j−1])<=m}+1,f[i])
(从
j
+
1
j + 1
j+1 个人到第
i
i
i 个人可以乘一辆车)
边界: f [ 0 ] = 0 f[0] = 0 f[0]=0, f [ 1 ] = 1 f[1] = 1 f[1]=1
Luogu P2679 子串 / (((小 ly 的题解 —— 膜拜 小 ly
其实是我粘的 luogu 的题解 qwq)
当然直接开
O
(
n
∗
m
∗
k
)
O(n* m * k)
O(n∗m∗k) 的数组是肯定开不下的,第一维的
i
i
i 显然可以滚掉。
但这道题我们可以倒序循环来压维。把
i
i
i 的一维直接压掉。
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7, N = 1005;
char a[N], b[N];
int s[N][N], f[N][N];
int main() {
int n, m, K;
scanf("%d%d%d", &n, &m, &K);
scanf("%s", a), scanf("%s", b);
s[0][0] = 1, f[0][0] = 1; // 初始状态什么都不选的情况下是 1
for(int i = 1; i <= n; i ++)
for(int j = m; j >= 1; j --)
for(int k = 1; k <= K; k ++) {
if(a[i - 1] != b[j - 1]) { // 因为 i j 表示的是 a b 两个字符串的位置,所以要 - 1
s[j][k] = 0; continue;
}
s[j][k] = (f[j - 1][k - 1] + s[j - 1][k]) % MOD;
f[j][k] = (s[j][k] + f[j][k]) % MOD;
}
printf("%d\n", f[m][K] % MOD); // 最后一个数可能不选,所以是 f[][]
return 0;
}
树形 DP
这里的依赖关系是以森林的形式给出的,我们增加一个虚拟节点作为所有无先修课的课程的父节点,搜索这棵树,用 f [ i ] [ m ] f[i][m] f[i][m] 表示选择第 i i i 个节点及其之后节点(兄弟或孩子)中的 m m m 个节点所对应的课程所获得的最大学分,则有两个转移方向:
- 给第 i i i 个节点和它的一个或多个子节点分配一定的课程数量 k k k,剩余课程数量 m − k − 1 m - k - 1 m−k−1 分给下一个兄弟节点。
- 不选择第 i i i 个节点,全部课程数量 m m m 分配给下一个兄弟节点。
f [ i ] [ m ] = max ( max { f [ i . c h i l d r e n ] [ k ] + f [ i . n e x t ] [ m − k − 1 ] , k ∈ [ 0 , m − 1 ] } , f [ i . n e x t ] [ m ] ) f[i][m]={\max}( {\max}\{ f[i.children][k] + f[i.next][m-k-1], k{\in}[0,m-1] \},f[i.next][m] ) f[i][m]=max(max{f[i.children][k]+f[i.next][m−k−1],k∈[0,m−1]},f[i.next][m])
(其实是和上面那道选课是一样的呀!
d
p
[
i
]
[
j
]
=
max
(
max
{
d
p
[
l
s
o
n
]
[
j
−
1
−
k
]
+
d
p
[
r
s
o
n
]
[
k
]
+
w
[
i
]
,
k
∈
[
0
,
j
−
1
]
}
,
d
p
[
i
]
[
j
]
)
dp[i][j] = {\max}( {\max}\{ dp[lson][j - 1 - k] + dp[rson][k] + w[i], k{\in}[0, j-1] \}, dp[i][j] )
dp[i][j]=max(max{dp[lson][j−1−k]+dp[rson][k]+w[i],k∈[0,j−1]},dp[i][j])
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示在
i
i
i 这个点,选
j
j
j 个点(这
j
j
j 个点包含第
i
i
i 个点,也就是说选
i
i
i 这个点并在
i
i
i 的子树里选
j
−
1
j - 1
j−1 个点)的最多苹果个数
注意: 因为你是要保留 m m m 条边,但你把边权转化成了点权,所以最终答案是保留 m + 1 m + 1 m+1 个点的最大苹果个树
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 205;
int head[MAXN], cnt = 0;
struct edge {
int to, next, w;
}e[MAXN << 1];
inline void add(int u, int v, int w) {
e[++ cnt] = (edge){v, head[u], w};
head[u] = cnt;
}
int n, m;
int dw[MAXN], size[MAXN];
int dp[MAXN][MAXN], lson[MAXN], rson[MAXN]; // lson[u] 是 u 的左儿子是谁
// dw[i] 是 i 的点权,把 x -> y 的边权放到 y 这个点上
void dfs1(int u, int fa) {
size[u] = 1;
for(int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if(v == fa)continue;
dw[v] = e[i].w;
if(!lson[u]) lson[u] = v;
else rson[u] = v;
dfs1(v, u);
size[u] += size[v];
}
}
// dp[i][j] = max(dp[i][j], dp[lson][j - 1 - k] + dp[rson][k] + w[i]) 0 <= k <= j - 1
// dp[i][j] 表示在 i 这个点,选 j 个点(这 j 个点包含第 i 个点,也就是说选 i 这个点并在 i 的子树里选 j - 1 个点)的最多苹果个数
void dfs2(int u, int fa) {
for(int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if(v == fa)continue;
dfs2(v, u);
for(int j = 2; j <= size[u]; j ++) {
for(int k = 0; k <= j - 1; k ++) {
dp[u][j] = max(dp[u][j], dp[lson[u]][j - 1 - k] + dp[rson[u]][k] + dw[u]);
}
}
}
}
int main() {
memset(dw, 0, sizeof(dw));
scanf("%d%d", &n, &m);
for(int i = 1; i < n; i ++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z), add(y, x, z);
}
dfs1(1, 1);
for(int i = 1; i <= n; i ++)
dp[i][1] = dw[i];
dfs2(1, 1);
printf("%d\n", dp[1][m + 1]); // 因为你是要保留 m 条边,但你把边权转化成了点权,所以是保留 m + 1 个点
return 0;
}
emmmmmm
其实 … 和上面两道题是一样的 …
输入格式是深搜顺序,这里我们可以递归调用来加边建树。
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示
i
i
i 为节点拿
j
j
j 副画所用最短时间
d
p
[
i
]
[
j
]
=
min
(
min
{
d
p
[
l
s
o
n
]
[
k
]
+
d
p
[
r
s
o
n
]
[
j
−
k
]
+
2
∗
(
l
s
o
n
.
w
+
r
s
o
n
.
w
)
,
k
∈
(
0
,
j
)
}
,
d
p
[
i
]
[
j
]
)
dp[i][j] = {\min}( {\min}\{ dp[lson][k] + dp[rson][j - k] + 2 * (lson.w + rson.w), k{\in}(0, j) \}, dp[i][j] )
dp[i][j]=min(min{dp[lson][k]+dp[rson][j−k]+2∗(lson.w+rson.w),k∈(0,j)},dp[i][j])
图论
题目:
给定一个n个点m条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
n < = 1 0 4 n<=10^4 n<=104,, m < = 1 0 5 m<=10^5 m<=105, ∣ 点 权 ∣ < = 1000 |点权|<=1000 ∣点权∣<=1000
分析:
缩点模板,先用 T a r j a n Tarjan Tarjan算法求出强连通分量,将强连通分量里每个点权值加到一起。然后枚举每一条边,如果两点属于不同强连通分量,就在强连通分量之间加一条边。
接下来有两种做法。
法一: S P F A SPFA SPFA
显然最长路径一定是以缩点后入度为 0 的点开始的,所以只需要对缩点后入度为 0 的点跑一遍
s
p
f
a
spfa
spfa,然后取最长路径,即为答案。
因为
N
<
=
10000
N<=10000
N<=10000,
M
<
=
100000
M<=100000
M<=100000,
T
a
r
j
a
n
Tarjan
Tarjan算法
O
(
N
)
O(N)
O(N)缩点后,点数边数大大减少,只需要跑入度为 0 的点,所以可以在时间限制内通过。
法二: D A G d p DAGdp DAGdp
只要在 D A G DAG DAG上做一次动态规划就行了,有点像 树 形 D P + 记 忆 化 树形DP+记忆化 树形DP+记忆化的感觉,就是缩点后 f o r for for循环从一个还没有被遍历过的强连通分量开始深搜,对于相邻的没有访问过的强连通分量,继续 D F S DFS DFS;对于相邻的访问过的强连通分量,则直接调用其权值,然后在所有强连通分量中取最大的权值,用当前强连通分量中所有点的值累加上这个权值,就是当前强连通分量最优的权值了。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, M = 1e5 + 5;
int head[N];
int stk[N];
int x[N], y[N];
int dfn[N], low[N], col[N];
int f[N], sum[N], w[N];
bool insta[N];
struct Edge {
int to, next;
}e[M << 1];
int cnt = 0, timer = 0;
int top = 0, tot = 0;
void add(int x, int y) {
e[++ cnt].to = y;
e[cnt].next = head[x];
head[x] = cnt;
}
void tarjan(int x) {
dfn[x] = low[x] = ++ timer;
stk[++ top] = x;
insta[x] = 1;
for(int i = head[x]; i; i = e[i].next) {
int v = e[i].to;
if(!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
} else if(insta[v]) low[x] = min(low[x], dfn[v]);
}
if(low[x] == dfn[x]) {
++ tot; int cmp = 0;
while(cmp != x) {
cmp = stk[top --];
col[cmp] = tot;
sum[tot] += w[cmp];
insta[cmp] = 0;
}
}
}
void search(int x) {
if(f[x]) return ;
f[x] = sum[x];
int maxsum = 0;
for(int i = head[x]; i; i = e[i].next) {
int v = e[i].to;
if(!f[v]) search(v);
maxsum = max(maxsum, f[v]);
}
f[x] += maxsum;
}
int main() {
memset(head, 0, sizeof(head));
memset(f, 0, sizeof(f));
memset(sum, 0, sizeof(sum));
memset(insta,0,sizeof(insta));
int n, m, ans = 0;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++)
scanf("%d", &w[i]);
for(int i = 1; i <= m; i ++) {
scanf("%d%d", &x[i], &y[i]);
add(x[i], y[i]);
}
for(int i = 1; i <= n; i ++)
if(!dfn[i]) tarjan(i);
memset(head, 0, sizeof(head));
memset(e, 0, sizeof(e));
for(int i = 1; i <= m; i ++)
if(col[x[i]] != col[y[i]]) add(col[x[i]], col[y[i]]);
for(int i = 1; i <= tot; i ++) {
if(!f[i]) {
search(i);
ans = max(ans, f[i]);
}
}
printf("%d\n", ans);
return 0;
}
/*
10 20
970 369 910 889 470 106 658 659 916 964
3 2
3 6
3 4
9 5
8 3
5 8
9 1
9 7
9 8
7 5
3 7
7 8
1 7
10 2
1 10
4 8
2 6
3 1
3 5
8 5
*/
贪心
分析:
法一:贪心
输入的花盆高度一定是由多个相连续的单调递增或递减的区间构成的
什么是多个相连续的单调递增或递减的区间呢 …
比如 7 10 17 16 18 17 1 2 3
就是由 |7 10 17| |17 16| |16 18| | 18 17| |17 1| | 1 2 3| 这几个连续的区间构成的
我们每次选取这样的一个区间的两端的高度即可
(因为区间是单调递增或递减的,所以两端的高度一定是最大或最小的,这样选就能保证一定最优,正确性显然)
法二:DP
emmmmm
其实和贪心思想一样
明白了贪心思想,这个 DP 应该是很显然的了 …
规定
f
[
i
]
[
0
]
f[i][0]
f[i][0] 或
f
[
i
]
[
1
]
f[i][1]
f[i][1] 表示以
i
i
i 为结尾的,应该寻找上升或者下降的花朵的时候最大能保留的花朵数。
(1 表示该找下降的花朵了,0 表示该找上升的花朵了)
最后答案就是
m
a
x
(
f
[
n
]
[
1
]
,
f
[
n
]
[
0
]
)
max(f[n][1], f[n][0])
max(f[n][1],f[n][0]),因为由贪心思想可得,最终答案一定会选最后一个区间的最后一朵花,即第
n
n
n 朵花。
if(a[i] > a[i - 1]) f[i][0] = f[i - 1][1] + 1;
else f[i][0] = f[i - 1][0];
if(a[i] < a[i - 1]) f[i][1] = f[i - 1][0] + 1;
else f[i][1] = f[i - 1][1];
分析: 贪心
如果第 i i i 个大臣放第 j j j 个大臣前面对答案的贡献小些,那么 i i i 个大臣就放第 j j j 个大臣前面
所以就是使
a
[
i
]
.
x
/
a
[
j
]
.
y
<
a
[
j
]
.
x
/
a
[
i
]
.
y
a[i].x / a[j].y < a[j].x / a[i].y
a[i].x/a[j].y<a[j].x/a[i].y
所以就是
a
[
i
]
.
x
∗
a
[
i
]
.
y
<
a
[
j
]
.
x
∗
a
[
j
]
.
y
a[i].x * a[i].y < a[j].x * a[j].y
a[i].x∗a[i].y<a[j].x∗a[j].y
然后高精度部分压位
乘法部分相当于高精度乘低精度
除法部分相当于高精度除低精度
高精度部分懒得写了
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int sum[N];
struct Score {
int x, y;
}a[N];
bool cmp(Score a, Score b) {
return a.x * a.y < b.x * b.y;
}
int main() {
int n;
scanf("%d", &n);
scanf("%d%d", &a[1].x, &a[1].y);
for(int i = 2; i <= n + 1; i ++)
scanf("%d%d", &a[i].x, &a[i].y);
sort(a + 2, a + n + 2, cmp); // 注意是到 a + n + 2
sum[1] = a[1].x;
for(int i = 2; i <= n + 1; i ++)
sum[i] = sum[i - 1] * a[i].x;
int f, ans = 0;
for(int i = 2; i <= n + 1; i ++) {
f = sum[i - 1] / a[i].y;
ans = max(ans, f);
}
printf("%d\n", ans);
return 0;
}
priority_queue <int, vector<int>, greater<int> > p;