标签: 动态规划

[CF2B]The least round way 题解

[CF2B]The least round way 题解

题目地址:Codeforces:Problem – 2B – Codeforces、洛谷:CF2B The least round way – 洛谷 | 计算机科学教育新生态

题目描述

There is a square matrix n × n, consisting of non-negative integer numbers. You should find such a way on it that

  • starts in the upper left cell of the matrix;
  • each following cell is to the right or down from the current cell;
  • the way ends in the bottom right cell.

Moreover, if we multiply together all the numbers along the way, the result should be the least “round”. In other words, it should end in the least possible number of zeros.

题意简述

给定一个$n \times n$的非负整数矩阵,要求你找出一条从左上角到右下角的路径,使得路径上的数字相乘得到的积的后缀0数量最少。

输入输出格式

输入格式:
The first line contains an integer number n (2 ≤ n ≤ 1000), n is the size of the matrix. Then follow n lines containing the matrix elements (non-negative integer numbers not exceeding 10^9).

输出格式:
In the first line print the least number of trailing zeros. In the second line print the correspondent way itself.

输入输出样例

输入样例#1:

3
1 2 3
4 5 6
7 8 9

输出样例#1:

0
DDRR

题解

考虑乘积的后缀0数量等于乘积中因数10的数量,则最小化乘积中质因子2和5的数量的思路很自然可以想出。答案即是最小化的2和5质因子数量中的较小值。
接下来考虑如何最小化答案。我们先用常规思路分别对质因子2和5的数量各自做一遍最小化,得到了两个答案,取其最小值即可。这个最小值一定可以取到,因为在取最小值时,最小化的那个质因子的数量一定比另外一个质因子小,这就保证了答案的正确性。
上面所说的“常规思路”是这样的一种动态规划思路:对于某一个点$(i, j)$,从左上角点$(1, 1)$到该处的路径上的最小质因子数量只能由左边的点$(i, j – 1)$和上面的点$(i – 1, j)$转移而来,即如果设$f(i, j)$是点$(i, j)$处的数字的某质因子数量,则上述状态的状态转移方程是
$$ dp(i, j) = f(i, j) + \min\{ dp(i – 1, j), dp(i, j – 1) \} $$

以为到这里就做完了?注意题目中有一个“非负整数”的坑,你还需要考虑路径中有0的情况。
如果路径中有0,会让路径的乘积变为0,即后缀0有1个。如果原本的最优路径的后缀0为0个,则原答案最优,否则应该经过这个数字为0的点。这个地方特例处理一下就可以了。
此外,注意处理一下边界情况无效状态不可转移即可。

算法总复杂度为$O(n^2)$。

代码

// Code by KSkun, 2019/6
#include <cstdio>
#include <cctype>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    LL res = 0, neg = 1; char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
    return res * neg;
}

inline char readsingle() {
    char c;
    while(!isgraph(c = fgc())) {}
    return c;
}

const int MAXN = 1005;

int n, a[MAXN][MAXN], dp2[MAXN][MAXN], dp5[MAXN][MAXN];
char pre2[MAXN][MAXN], pre5[MAXN][MAXN], ans[MAXN << 1];
int atot = 0;
int zx = 0, zy = 0;

inline int calfac(int x, int f) {
    if(x == 0) return 0; // 注意x=0时进入死循环
    int cnt = 0;
    while(x % f == 0) {
        cnt++; x /= f;
    }
    return cnt;
}

int main() {
    n = readint();
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            a[i][j] = readint();
            if(a[i][j] == 0) {
                zx = i; zy = j;
            }
            dp2[i][j] = calfac(a[i][j], 2);
            dp5[i][j] = calfac(a[i][j], 5);
        }
    }
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            int r1 = dp2[i][j] + dp2[i - 1][j], r2 = dp2[i][j] + dp2[i][j - 1];
            if(i == 1) r1 = 1e9; // 边界情况无效状态设置为不可转移
            if(j == 1) r2 = 1e9;
            if(r1 < r2) {
                dp2[i][j] += dp2[i - 1][j]; 
                pre2[i][j] = 'D';
            } else {
                dp2[i][j] += dp2[i][j - 1]; 
                pre2[i][j] = 'R';
            }
        }
    }
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            int r1 = dp5[i][j] + dp5[i - 1][j], r2 = dp5[i][j] + dp5[i][j - 1];
            if(i == 1) r1 = 1e9; // 边界情况无效状态设置为不可转移
            if(j == 1) r2 = 1e9;
            if(r1 < r2) {
                dp5[i][j] += dp5[i - 1][j];
                pre5[i][j] = 'D';
            } else {
                dp5[i][j] += dp5[i][j - 1];
                pre5[i][j] = 'R';
            }
        }
    }
    int t;
    if(dp2[n][n] < dp5[n][n]) t = 2; else t = 5;
    if(zx && zy && (t == 2 ? dp2[n][n] : dp5[n][n]) > 1) {
        puts("1"); // 当包含0的路径是最优情况时的特殊处理
        for(int i = 1; i < zx; i++) putchar('D');
        for(int i = 1; i < zy; i++) putchar('R');
        for(int i = zx; i < n; i++) putchar('D');
        for(int i = zy; i < n; i++) putchar('R');
    } else {
        printf("%d\n", t == 2 ? dp2[n][n] : dp5[n][n]);
        int nx = n, ny = n;
        while(nx != 1 || ny != 1) {
            char op = t == 2 ? pre2[nx][ny] : pre5[nx][ny];
            ans[++atot] = op;
            if(op == 'D') nx--;
            else ny--;
        }
        for(int i = atot; i >= 1; i--) putchar(ans[i]);
    }
    return 0;
}
[ZJOI2007]时态同步 题解

[ZJOI2007]时态同步 题解

题目地址:洛谷:【P1131】[ZJOI2007]时态同步 – 洛谷、BZOJ:Problem 1060. — [ZJOI2007]时态同步

题目描述

小Q在电子工艺实习课上学习焊接电路板。一块电路板由若干个元件组成,我们不妨称之为节点,并将其用数字1,2,3….进行标号。电路板的各个节点由若干不相交的导线相连接,且对于电路板的任何两个节点,都存在且仅存在一条通路(通路指连接两个元件的导线序列)。
在电路板上存在一个特殊的元件称为“激发器”。当激发器工作后,产生一个激励电流,通过导线传向每一个它所连接的节点。而中间节点接收到激励电流后,得到信息,并将该激励电流传向与它连接并且尚未接收到激励电流的节点。最终,激烈电流将到达一些“终止节点”——接收激励电流之后不再转发的节点。
激励电流在导线上的传播是需要花费时间的,对于每条边e,激励电流通过它需要的时间为te,而节点接收到激励电流后的转发可以认为是在瞬间完成的。现在这块电路板要求每一个“终止节点”同时得到激励电路——即保持时态同步。由于当前的构造并不符合时态同步的要求,故需要通过改变连接线的构造。目前小Q有一个道具,使用一次该道具,可以使得激励电流通过某条连接导线的时间增加一个单位。请问小Q最少使用多少次道具才可使得所有的“终止节点”时态同步?

题意简述

有一棵$n$个点有边权的树,你可以每次给一条边边权+1,求最小的加边权次数,使得根到每个叶子的路径边权和相等。

输入输出格式

输入格式:
第一行包含一个正整数N,表示电路板中节点的个数。第二行包含一个整数S,为该电路板的激发器的编号。接下来N-1行,每行三个整数a , b , t。表示该条导线连接节点a与节点b,且激励电流通过这条导线需要t个单位时间。

输出格式:
仅包含一个整数V,为小Q最少使用的道具次数。

输入输出样例

输入样例#1:

3
1
1 2 1
1 3 3

输出样例#1:

2

说明

N ≤ 500000,te ≤ 1000000

题解

对于一个子树,我们求出根到叶子的路径和最大值,再把不满最大值的根的出边加满即可,可以证明,如此做得到的就是最优解。这是因为,如果叶子路径和都不同,不在这个子树根加成相同的话,就得在更深的位置加成相同的,操作的边数会大于子树根的出边数,显然不够优。
复杂度$O(n)$。

代码

// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>
#include <vector>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
    return res * neg;
}

inline char readsingle() {
    register char c;
    while(!isgraph(c = fgc())) {}
    return c;
}

const int MAXN = 500005;

int n, s;

struct Edge {
    int to, w;
};
std::vector<Edge> gra[MAXN];

int mx[MAXN]; LL ans;

void dfs(int u, int fa) {
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i].to;
        if(v == fa) continue;
        dfs(v, u);
        mx[u] = std::max(mx[u], mx[v] + gra[u][i].w);
    }
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i].to;
        if(v == fa) continue;
        ans += mx[u] - (mx[v] + gra[u][i].w);
    }
}

int main() {
    n = readint(); s = readint();
    for(int i = 1, u, v, w; i < n; i++) {
        u = readint(); v = readint(); w = readint();
        gra[u].push_back(Edge {v, w});
        gra[v].push_back(Edge {u, w});
    }
    dfs(s, 0);
    printf("%lld", ans);
    return 0;
}
[洛谷4643]【模板】动态dp 题解

[洛谷4643]【模板】动态dp 题解

题目地址:洛谷:【P4643】【模板】动态dp – 洛谷

题目描述

给定一棵 n 个点的树,点带点权。
有 m 次操作,每次操作给定 x, y ,表示修改点 x 的权值为 y 。
你需要在每次操作之后求出这棵树的最大权独立集的权值大小。

输入输出格式

输入格式:
第一行, n, m ,分别代表点数和操作数。
第二行, V1, V2, …, Vn ,代表 n 个点的权值。
接下来 n-1 行, x, y ,描述这棵树的 n-1 条边。
接下来 m 行, x, y ,修改点 x 的权值为 y 。

输出格式:
对于每个操作输出一行一个整数,代表这次操作后的树上最大权独立集。
保证答案在 int 范围内

输入输出样例

输入样例#1:

10 10
-11 80 -99 -76 56 38 92 -51 -34 47 
2 1
3 1
4 3
5 2
6 2
7 1
8 2
9 4
10 7
9 -44
2 -17
2 98
7 -58
8 48
3 99
8 -61
9 76
9 14
10 93

输出样例#1:

186
186
190
145
189
288
244
320
258
304

说明

对于30%的数据, 1≤n,m≤10
对于60%的数据, 1≤n,m≤1000
对于100%的数据, 1≤n,m≤10^5

题解

参考资料:Luogu P4643 【模板】动态dp – 胡小兔 – 博客园
首先这个题不带修改就是一个“没有上司的舞会”模型,用$dp[u][0/1]$代表$u$这个点选或不选其子树中的最大权独立集的权值和,DFS向上转移即可。
现在我们需要动态地维护树上的DP值,我们需要一种方法来使权值能快速更新。在这道题里,我们的转移方程是
$$ \begin{aligned}
dp[u][0] &= \sum_{v \text{ is a son of } u} \max \{ dp[v][0], dp[v][1] \} \\
dp[u][1] &= \sum_{v \text{ is a son of } u} dp[v][0]
\end{aligned} $$
首先不好搞的是儿子的数量比较多求和不好做,我们先考虑把树给剖掉,使用$g[u][0/1]$记录$u$这个点选或不选其自身和所有轻儿子的答案的和,而$f[u][0/1]$则记录总的答案,即合并轻重儿子考虑。现在,方程变成了这样
$$ \begin{aligned}
f[u][0] &= g[u][0] + \max \{ f[v][0], f[v][1] \} \\
f[u][1] &= g[u][1] + f[v][0]
\end{aligned} $$
其中的$v$是$u$的重儿子。到现在为止,似乎也没有把转移的形式变得可以树上操作,而且怎么求$g[u][0/1]$的值也不知道。这时,我们想到了矩阵。
矩阵的乘法可以理解为向量运算,即$\mathbf{A} \times \mathbf{B}$可以理解为$\mathbf{B}$中的每一个行向量$\mathbf{b_1}$乘上$\mathbf{A}$中每一行对应的系数$\{ a_{1, 1}, a_{1, 2}, \dots, a_{1, n} \}$再加起来得到的行向量组(或者也可以用列向量的形式理解)。
在这里,我们需要重新定义向量加法和标量乘法。
$$ \begin{aligned}
\mathbf{a} + \mathbf{b} = \mathbf{c} &\Rightarrow c_i = \max \{ a_i, b_i \} \\
\mathbf{a} \times b = \mathbf{c} &\Rightarrow c_i = a_i + b
\end{aligned} $$
我们发现上面的重新定义的运算具有很好的性质,可以按照这样的定义构造向量空间,来定义新的矩阵乘法。新的矩阵乘法如下
$$ \mathbf{A} \times \mathbf{B} = \mathbf{C} \Rightarrow C_{i, j} = \max_k \{ A_{i, k} + B_{k, j} \} $$
这样,我们就可以构造一个矩阵来进行DP的转移了
$$ \begin{bmatrix} f[u][0] \\ f[u][1] \end{bmatrix} = \begin{bmatrix} f[v][0] \\ f[v][1] \end{bmatrix} \times \begin{bmatrix} g[u][0] & g[u][0] \\ g[u][1] & 0 \end{bmatrix} $$
那么,只要我们知道了整棵树上的转移矩阵(即右侧的那个$2 \times 2$矩阵),就可以通过线段树维护重链矩阵乘法,从而快速求某条重链顶端的答案了。我们无需设置一个列向量来乘上这个转移矩阵,这是因为,叶子节点的$f[u][0]=g[u][0]=0, f[u][1]=g[u][1]=w_u$,看上去就跟列向量一样了。
接下来考虑修改问题,我们考虑更新这个结点的矩阵后,沿重链上跳,每个重点的顶的父亲都会受到影响,此时只需要把影响大力算出来改一下就好。
我的初始化是逐个点改权值,复杂度是$O(n \log^2 n)$的,实际上可以先$O(n)$地做一遍DP,然后用DP值构造矩阵,这个复杂度是$O(n+n \log n)$,相比而言常数更优秀,不过代码更长,考虑到这个题没卡常,就懒得写了233
总复杂度是$O((n+m) \log^2 n)$。

代码

// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>
#include <vector>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
    return res * neg;
}

const int MAXN = 100005;

struct Matrix {
    LL m[2][2];
    Matrix() {
        memset(m, 0, sizeof(m));
    }
    inline LL* operator[](const int &x) const {
        return const_cast<LL*>(m[x]);
    }
    inline Matrix operator*(const Matrix &rhs) const {
        Matrix res;
        for(int i = 0; i <= 1; i++) {
            for(int j = 0; j <= 1; j++) {
                for(int k = 0; k <= 1; k++) {
                    res[i][j] = std::max(res[i][j], m[i][k] + rhs[k][j]);
                }
            }
        }
        return res;
    }
    inline Matrix& operator*=(Matrix &rhs) {
        return *this = *this * rhs;
    }
};

int n, m, w[MAXN];
std::vector<int> gra[MAXN];

int fa[MAXN], siz[MAXN], dep[MAXN], son[MAXN], top[MAXN], end[MAXN], dfn[MAXN], vn[MAXN], clk;

void dfs1(int u) {
    siz[u] = 1;
    for(auto v : gra[u]) {
        if(v == fa[u]) continue;
        fa[v] = u; dep[v] = dep[u] + 1;
        dfs1(v);
        if(siz[v] > siz[son[u]]) son[u] = v;
    }
}

void dfs2(int u, int tp) {
    dfn[u] = ++clk; vn[dfn[u]] = u; top[u] = tp; 
    end[tp] = std::max(end[tp], dfn[u]);
    if(son[u]) {
        dfs2(son[u], tp);
    }
    for(auto v : gra[u]) {
        if(v == fa[u] || v == son[u]) continue;
        dfs2(v, v);
    }
}

Matrix tr[MAXN << 2], val[MAXN];

#define lch o << 1
#define rch o << 1 | 1
#define mid ((l + r) >> 1)

void modify(int o, int l, int r, int x) {
    if(l == r) {
        tr[o] = val[vn[l]]; return;
    }
    if(x <= mid) modify(lch, l, mid, x);
    else modify(rch, mid + 1, r, x);
    tr[o] = tr[lch] * tr[rch];
}

Matrix query(int o, int l, int r, int ll, int rr) {
    if(l == ll && r == rr) return tr[o];
    if(rr <= mid) return query(lch, l, mid, ll, rr);
    else if(ll > mid) return query(rch, mid + 1, r, ll, rr);
    else return query(lch, l, mid, ll, mid) * query(rch, mid + 1, r, mid + 1, rr);
}

inline Matrix query(int u) {
    return query(1, 1, n, dfn[top[u]], end[top[u]]);
}

inline void modify(int u, int v) {
    val[u][1][0] += v - w[u];
    w[u] = v;
    Matrix m1, m2;
    while(u) {
        m1 = query(top[u]);
        modify(1, 1, n, dfn[u]);
        m2 = query(top[u]);
        u = fa[top[u]];
        val[u][0][0] += std::max(m2[0][0], m2[1][0]) - std::max(m1[0][0], m1[1][0]);
        val[u][0][1] = val[u][0][0];
        val[u][1][0] += m2[0][0] - m1[0][0];
    }
}

int main() {
    n = readint(); m = readint();
    for(int i = 1; i <= n; i++) {
        w[i] = readint();
    }
    for(int i = 1, u, v; i < n; i++) {
        u = readint(); v = readint();
        gra[u].push_back(v); gra[v].push_back(u);
    }
    dfs1(1);
    dfs2(1, 1);
    for(int i = 1; i <= n; i++) {
        int t = w[i]; w[i] = 0;
        modify(i, t);
    }
    while(m--) {
        int x = readint(), y = readint();
        modify(x, y);
        Matrix res = query(1);
        printf("%lld\n", std::max(res[0][0], res[1][0]));
    }
    return 0;
}