找规律大法好啊!
题目
T1:BZOJ 4518 / SDOI 2016 征途 (journey)(斜率优化DP)
T2:BZOJ 1434 / ZJOI 2009 染色游戏 (game)(SG函数+找规律)
T3:BZOJ 2286 / SDOI 2011 消耗战 (repair)(虚树+树形DP)
T1
分析
首先拆开方差的式子得到:
1m∑mi=1(xi−x¯)2=1m∑mi=1(x2i+x¯2−2x¯xi)
1
m
∑
i
=
1
m
(
x
i
−
x
¯
)
2
=
1
m
∑
i
=
1
m
(
x
i
2
+
x
¯
2
−
2
x
¯
x
i
)
=∑mi=1x2i−x¯∑mi=1xim
=
∑
i
=
1
m
x
i
2
−
x
¯
∑
i
=
1
m
x
i
m
乘上
m2
m
2
后得到
m∑mi=1x2i−(∑mi=1xi)2
m
∑
i
=
1
m
x
i
2
−
(
∑
i
=
1
m
x
i
)
2
。
所以问题转化为,每一天走的路的长度的平方和尽可能小。
先预处理路长度的前缀和
sum[]
s
u
m
[
]
。设
f[i,k]
f
[
i
,
k
]
为到了第
i
i
段路,走了天的最小平方和,其中
i≥k
i
≥
k
。
容易得出转移:
f[i,k]=mini−1j=k−1(f[j,k−1]+(sum[i]−sum[j])2)
f
[
i
,
k
]
=
min
j
=
k
−
1
i
−
1
(
f
[
j
,
k
−
1
]
+
(
s
u
m
[
i
]
−
s
u
m
[
j
]
)
2
)
。
可以发现,上面方程的状态数为
O(nm)
O
(
n
m
)
,转移数为
O(n)
O
(
n
)
,总复杂度为
O(n2m)
O
(
n
2
m
)
,TLE。
看到
(sum[i]−sum[j])2
(
s
u
m
[
i
]
−
s
u
m
[
j
]
)
2
这样的式子,容易知道往斜率优化的方向去思考。
由于
f[...,k]
f
[
.
.
.
,
k
]
只能从
f[...,k−1]
f
[
.
.
.
,
k
−
1
]
转移,所以这里第一层枚举
k
k
进行DP,设为
F[i]
F
[
i
]
,
f[i,k−1]
f
[
i
,
k
−
1
]
为
G[i]
G
[
i
]
。
那么转移方程化为
F[i]=mini−1j=k−1(G[j]+(sum[i]−sum[j])2)
F
[
i
]
=
min
j
=
k
−
1
i
−
1
(
G
[
j
]
+
(
s
u
m
[
i
]
−
s
u
m
[
j
]
)
2
)
。
将
G[j]+(sum[i]−sum[j])2
G
[
j
]
+
(
s
u
m
[
i
]
−
s
u
m
[
j
]
)
2
展开得到:
G[j]+sum[i]2+sum[j]2−2sum[i]sum[j]
G
[
j
]
+
s
u
m
[
i
]
2
+
s
u
m
[
j
]
2
−
2
s
u
m
[
i
]
s
u
m
[
j
]
又发现
sum[i]2
s
u
m
[
i
]
2
是和决策(
j
j
)无关的东西,所以:
设:
K[i]=2sum[i]
K
[
i
]
=
2
s
u
m
[
i
]
B[i]=G[i]+sum[i]2
B
[
i
]
=
G
[
i
]
+
s
u
m
[
i
]
2
那么
min
min
内的式子可以表示为
−K[j]sum[i]+B[j]
−
K
[
j
]
s
u
m
[
i
]
+
B
[
j
]
。
把每一个决策都
j
j
都看作一条直线,那么可以看出,现在的最优决策就是在
x=sum[i]
x
=
s
u
m
[
i
]
的条件下,
y
y
值最大的直线(因为有最大值则
−K[j]x+B[j]
−
K
[
j
]
x
+
B
[
j
]
有最小值)。
不难发现,
K[j]
K
[
j
]
和
B[j]
B
[
j
]
都随
j
j
不降,并且每一个对应的
x=sum[i]
x
=
s
u
m
[
i
]
也不降。
下面用一个单调队列维护所有决策直线组成的凸壳。
首先介绍怎样加入新决策。
假设单调队列的队尾存在两个决策
p1,p2
p
1
,
p
2
,那么新决策
j
j
存在着两种可能:
(1)
(2)
(1)中,可以发现,为不同的值时,
p1,p2,j
p
1
,
p
2
,
j
这三个决策都有可能是最优决策,并且
x
x
越大,最优决策越往后走。所以这时直接把加入单调队列的队尾。
(2)中,可以发现,
x
x
不管是多少,都绝对不是最优决策。所以这时候把
p2
p
2
出队后再把
j
j
入队。
判断(1)(2)中的哪一种情况,可以用叉积实现。
对于寻找最优决策,因为递增并且
x
x
越大最优决策越往后走,所以在队首不断出队以找到最优决策。
这样,总复杂度完美解决。
Source
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll;
const int N = 3005;
int n, m, a[N];
ll sum[N], X[N], Y[N], f[N][N], H, T, que[N];
ll calc(int j, int i) {
return Y[j] - X[j] * sum[i];
}
bool slope(int p1, int p2, int p3) {
ll x1 = (Y[p1] - Y[p2]) * (X[p2] - X[p3]),
x2 = (Y[p2] - Y[p3]) * (X[p1] - X[p2]);
return x1 >= x2;
}
int main() {
int i, j; n = read(); m = read();
for (i = 1; i <= n; i++) sum[i] = sum[i - 1] + (a[i] = read());
for (i = 1; i <= n; i++) f[i][1] = sum[i] * sum[i];
for (j = 2; j <= m; j++) {
H = T = 1; que[1] = j - 1; X[j - 1] = sum[j - 1] * 2;
Y[j - 1] = f[j - 1][j - 1] + sum[j - 1] * sum[j - 1];
for (i = j; i <= n; i++) {
while (H < T && calc(que[H], i) >= calc(que[H + 1], i)) H++;
f[i][j] = calc(que[H], i) + sum[i] * sum[i];
X[i] = sum[i] * 2; Y[i] = f[i][j - 1] + sum[i] * sum[i];
while (H < T && slope(que[T - 1], que[T], i)) T--;
que[++T] = i;
}
}
cout << f[n][m] * m - sum[n] * sum[n];
return 0;
}
T2
分析
定义
SG(x,y)
S
G
(
x
,
y
)
表示只有
(x,y)
(
x
,
y
)
这个位置的硬币为反面朝上,其他硬币都是正面朝上,这种状态下的
SG
S
G
值(坐标从
0
0
开始计数)。
通过找规律,可以发现:
时,
SG(x,y)=lowbit(x+y+1)
S
G
(
x
,
y
)
=
l
o
w
b
i
t
(
x
+
y
+
1
)
xy≠0
x
y
≠
0
时,
SG(x,y)=2x+y
S
G
(
x
,
y
)
=
2
x
+
y
由于
2x+y
2
x
+
y
较大,所以求所有反面朝上的硬币的
SG
S
G
值得异或和时,要用一个数组模拟异或的过程。
Source
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 205;
char s[N][N]; int cnt[N], w[N];
int lowbit(int x) {return x & -x;}
void SG(int x, int y) {
if (x * y) cnt[x + y] ^= 1;
else cnt[w[lowbit(x + y + 1)]] ^= 1;
}
int main() {
int i, j, n, m, T = read();
for (i = 1, j = 0; i <= 201; i <<= 1, j++) w[i] = j;
while (T--) {
n = read(); m = read(); memset(cnt, 0, sizeof(cnt));
for (i = 0; i < n; i++) scanf("%s", s[i]);
for (i = 0; i < n; i++) for (j = 0; j < m; j++)
if (s[i][j] == 'T') SG(i, j);
bool flag = 0; for (i = 0; i <= 201; i++)
if (cnt[i]) flag = 1;
printf(flag ? "-_-\n" : "=_=\n");
}
return 0;
}
T3
分析
看到题目的条件,容易想到是一道树形DP题,设
f[u]
f
[
u
]
为让
u
u
不能到达的子树内(不包括
u
u
)的任意关键点(能源丰富的岛屿)的最小代价。如果是叶子节点,那么
f[u]=0
f
[
u
]
=
0
。
转移就是枚举子节点
v
v
,设为边
(u,v)
(
u
,
v
)
的权值,则转移为:
如果
v
v
是关键点,那么。
否则
f[u]+=min(val(u,v),f[v])
f
[
u
]
+
=
min
(
v
a
l
(
u
,
v
)
,
f
[
v
]
)
。
考虑到询问次数较大,但给出关键点的总数很少,因此可以构建出一个虚树。注意把
1
1
<script type="math/tex" id="MathJax-Element-114">1</script>号节点加入关键点。
如果对虚树不了解,可参考神犇zzq的博客:https://www.cnblogs.com/zzqsblog/p/5560645.html
构建完虚树后,就可以在虚树上DP了。注意虚树上一条边的边权为原树上这两点的路径上边权的最小值。
Source
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll;
const int N = 3e5 + 5, M = 5e5 + 5, LogN = 21;
int n, ecnt, nxt[M], adj[N], go[M], val[M], dep[N], fa[N][LogN], mx[N][LogN],
tn, tmp[N], vn, vir[N], dfn[N], times, top, stk[N], par[N]; ll f[N];
bool isvir[N];
void add_edge(int u, int v, int w) {
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; val[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; val[ecnt] = w;
}
void dfs(int u, int fu) {
int i; dep[u] = dep[fa[u][0] = fu] + 1; dfn[u] = ++times;
for (i = 0; i <= 18; i++) {
fa[u][i + 1] = fa[fa[u][i]][i];
mx[u][i + 1] = min(mx[u][i], mx[fa[u][i]][i]);
}
for (int e = adj[u], v; e; e = nxt[e]) {
if ((v = go[e]) == fu) continue;
mx[v][0] = val[e]; dfs(v, u);
}
}
int lca(int u, int v) {
int i; if (dep[u] < dep[v]) swap(u, v);
for (i = 19; i >= 0; i--) {
if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
if (u == v) return u;
}
for (i = 19; i >= 0; i--)
if (fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
int dis(int u, int v) {
int i, d = dep[u] - dep[v], res = 0x3f3f3f3f;
for (i = 19; i >= 0; i--)
if ((d >> i) & 1) res = min(res, mx[u][i]), u = fa[u][i];
return res;
}
inline bool comp(const int &x, const int &y) {
return dfn[x] < dfn[y];
}
void build_virtree() {
int i; sort(vir + 1, vir + vn + 1, comp); top = 0;
for (i = 1; i <= tn; i++) {
int u = vir[i]; if (!top) {par[stk[++top] = u] = 0; continue;}
int w = lca(stk[top], u);
while (dep[stk[top]] > dep[w]) {
if (dep[stk[top - 1]] < dep[w]) par[stk[top]] = w;
top--;
}
if (w != stk[top]) vir[++vn] = w, par[w] = stk[top], stk[++top] = w;
par[stk[++top] = u] = w;
}
sort(vir + 1, vir + vn + 1, comp);
}
ll orzdalao() {
int i; for (i = 1; i <= vn; i++) f[vir[i]] = 0;
for (i = vn; i > 1; i--) {
int v = vir[i], u = par[v], w = dis(v, u);
if (isvir[v]) f[u] += w;
else f[u] += min(1ll * w, f[v]);
}
return f[1];
}
int main() {
int i, x, y, z; n = read();
for (i = 1; i < n; i++) x = read(), y = read(),
z = read(), add_edge(x, y, z); dfs(1, 0);
int q = read(); while (q--) {
vn = tn = read() + 1; vir[1] = tmp[1] = 1;
for (i = 2; i <= vn; i++) isvir[vir[i] = tmp[i] = read()] = 1;
build_virtree(); printf("%I64d\n", orzdalao());
for (i = 2; i <= tn; i++) isvir[tmp[i]] = 0;
}
return 0;
}