T1:路径问题(二)

特殊的图,每个点的出度为 \(1\),有向图
一些普遍的图论算法可能不好用

注意题目本身的条件的特殊性,环不需要依靠通用的很复杂的算法,我们可以直观暴力地找

环上的点,它们的答案其实就是环的大小
环外的点,答案为环大小+到环的距离

其实就是记忆化
\(v\) 出发,不停走后继,如果一路上都是答案不确定的点,那么我们就 \(O(n)\) 地处理所有遇到的点的答案

如果走着走着遇到一个已经知道答案的点?
利用这个答案就好了

代码实现
#include <bits/extc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using std::cin;
using std::cout;
using std::vector;
using ll = long long;

int main() {
    int n;
    cin >> n;
    
    vector<int> t(n);
    rep(i, n) cin >> t[i], t[i]--;
    
    vector<int> ans(n);
    vector<int> memo(n, -1);
    int cnt = 0;
    vector<int> ord(n);
    rep(v, n) {
        if (ans[v] == 0) { // 这是一个不知道 ans 的点,有必要从它出发走一走看看
            int u = v;
            vector<int> s;
            while (memo[u] == -1) {
                memo[u] = cnt; // 记录下来在哪一批处理掉
                ord[u] = s.size(); // 记录下来这个点是一路上第几个遇到的,方便后面求环的大小
                s.push_back(u); // 记录一路下来我们遇到了哪些点
                u = t[u];
            }
            // 接下来保证了 memo[u]>=0,分两类,这个点是我们新遇到的一个环,还是一个以前已经处理过的点
            
            //  1. 我们新遇到的一个环上的一个点
            if (memo[u] == cnt) {
                int c = s.size()-ord[u];
                for (int i = ord[u]; i < s.size(); ++i) ans[s[i]] = c; // 先把环上的点处理好
                rep(i, ord[u]) ans[s[i]] = c+(ord[u]-i);
            }
            //  2. 一个以前处理过的新点
            else {
                rep(i, s.size()) ans[s[i]] = ans[u]+(s.size()-i);
            }
            cnt++;
        }
    }
    
    rep(v, n) cout << ans[v] << '\n';
    
    return 0;
}

T2: 平整序列(二)

操作分为两步:

  1. 选择 \(m\) 个数,任意修改它们的数值
  2. 区间修改

目标是把所有数变成 \(0\)

\(m = 0\)时,其实就是 铺设道路

回到本题,\(m\) 次修改,怎么办?
把握好以下几点:

  • \(n\) 只有 \(300\),复杂度可能是 \(O(n^3)\)
  • 修改数字的时候,如果把一个数改成和相邻的数一样大?其实效果上相当于删掉了一个数。
  • \(m\) 次修改,假设答案为 \(ans'\)
    \(ans \leqslant ans'\)
    \(ans \geqslant ans'\)
    这里需要一点数学证明,效果上,修改最多相当于删除

\(n\) 个数,选择 \(m\) 个数把它们删了,使得 \(\sum \max(0, a_i-a_{i-1})\) 最小

可以考虑 \(dp\)
dp[i][j] 表示前 \(i-1\) 个数里面,删了 \(j\) 个数时 \(\sum \max(0, a_i-a_{i-1})\) 最小值

最后的答案就是 \(dp[n+1][m]\)

转移方程:

\( dp[i][j] = \min\{dp[i-1-k][j-k]+\max(0, a_i-a_{i-1-k})\} \)

特例:\(j = i-1\) 时,\(dp[i][j] = \max(0, a_i-a_0)\)

代码实现
#include <bits/extc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)

using std::cin;
using std::cout;
using std::max;
using std::vector;
using ll = long long;

const ll INF = 1e18;

inline void chmin(ll& x, ll y) { if (x > y) x = y; }

const int MX = 305;

ll dp[MX][MX];

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<int> a(n+2);
    rep(i, n) cin >> a[i];
    
    rep(i, n+1) {
        for (int j = 0; j <= i-1; ++j) {
            if (j == i-1) dp[i][j] = max(0, a[i]-a[0]);
            else {
                ll now = INF;
                for (int k = 0; k <= j; ++k) {
                    chmin(now, dp[i-1-k][j-k] + max(0, a[i]-a[i-1-k]));
                }
                dp[i][j] = now;
            }
        }
    }
    
    cout << dp[n+1][m] << '\n';
    
    return 0;
}

T3:方形最大值

本题是“求一个区域的最值”的二维版本
一维版本:
给定 \(n\) 个数 \(a_1, a_2, \cdots, a_n\) 和一个整数 \(k\),要求所有连续 \(k\) 个数的最值
显然是用单调队列做

以最大值为例
如果 \(a_j\)\(a_i\) 右边,且 \(a_j > a_i\),那么我们考虑所有右端点大于等于 \(j\) 的长度为 \(k\) 的区间。这些区间的最大值是不用考虑 \(a_i\)
所以我们的考虑内容应该是一个向右递减的队列

怎么变到二维?
\(b_{ij}\) 表示 \(a_{i,j-k+1}, \cdots, a_{ij}\)\(k\) 个数里的最大值,\(j \geqslant k\)
那么我们只需要对 \(b_{i-k+1, j}, \cdots, b_{ij}\) 再求一次最大值
就是 \(ans[i][j]\)

代码实现
#include <bits/extc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using std::cin;
using std::cout;
using std::vector;
using std::deque;

inline void read(int &x) {
    x = 0;
    char c = getchar();
    while (c < '0' or c > '9') {
        c = getchar();
    }
    while ('0' <= c and c <= '9') {
        x = (x<<3) + (x<<1) + (c^48);
        c = getchar();
    }
}

struct Node {
    int id; // 代表第几个数
    int x;
};

deque<Node> q;

int main() {
    int n, k;
    cin >> n >> k;
    
    vector a(n, vector<int>(n));
    rep(i, n)rep(j, n) read(a[i][j]);
    
    // b[i][j] 表示 a[i][j-k+1],...,a[i][j] 这连续 k 个数的最大值(横着)
    vector b(n, vector<int>(n));
    // 用单调队列的思想处理出 b 数组
    rep(i, n) {
        rep(j, k) { // 单调队列的前 k 个数
            while (q.size() and q.back().x < a[i][j]) q.pop_back();
            q.push_back({j, a[i][j]});
        }
        b[i][k-1] = q.front().x;
        for (int j = k; j < n; ++j) {
            while (q.size() and q.back().x < a[i][j]) q.pop_back();
            q.push_back({j, a[i][j]});
            if (q.front().id+k <= j) q.pop_front();
            b[i][j] = q.front().x;
        }
        q.clear();
    }
    
    // c[i][j] 表示 b[i][j-k+1],...,b[i][j] 这连续 k 个数的最大值(竖着)
    vector c(n, vector<int>(n));
    // 用单调队列的思想处理出 c 数组
    rep(j, n) {
        rep(i, k) { // 单调队列的前 k 个数
            while (q.size() and q.back().x < b[i][j]) q.pop_back();
            q.push_back({i, b[i][j]});
        }
        c[k-1][j] = q.front().x;
        for (int i = k; i < n; ++i) {
            while (q.size() and q.back().x < b[i][j]) q.pop_back();
            q.push_back({i, b[i][j]});
            if (q.front().id+k <= i) q.pop_front();
            c[i][j] = q.front().x;
        }
        q.clear();
    }
    
    for (int i = k-1; i < n; ++i) {
        for (int j = k-1; j < n; ++j) {
            cout << c[i][j] << " \n"[j == n-1];
        }
    }
    
    return 0;
}

T4:零的数量

本题可以直接做,暴力地去算:个位 \(0\) 有多少个,十位 \(0\) 有多少个,\(\cdots\)

法一:暴力

个位0\(\lfloor\frac{n}{10}\rfloor\)
中间的位置0:根据 \(n\) 的这一位是否为 \(0\),分类讨论,如果不为 0,则简单;如果为 0,则前面的位置填到 \(n\) 的极限的时候,要特殊处理一下

代码实现
#include <bits/extc++.h>

using std::cin;
using std::cout;
using ll = long long;

int main() {
    ll n;
    cin >> n;
    
    // 接下来一位一位地考虑
    // 我们用 m=1 的地方,表示我们现在处理的是哪一位
    // 比如 m=10,表示我们在处理十位的0,m=1 表示我们在处理个位的0
    ll ans = 0;
    for (ll m = 1; m <= n/10; m *= 10) {
        if (m == 1) ans += n/10;
        else {
            ll a = n/m, b = n%m; // 把 n 根据这一位拆成两半,a 的最低位就是 m 考虑的数位
            // 判断 n 的这一位是否为0
            if (a%10 == 0) {
                ans += (a/10-1)*m + b + 1;
            }
            else {
                ans += a/10*m;
            }
        }
    }
    
    cout << ans << '\n';
    
    return 0;
}

法二:数位dp

dfs(int pos, bool prezero, bool limit) 的返回值,表示前 \(pos\) 个位置已经填完了余下的位置任意填,可以发现的 0 的个数,prezero == true 表示前面填的都是 \(0\),也就是前导 \(0\)
limit == true 表示前面填的东西都是 \(n\) 的东西

代码实现
#include <bits/extc++.h>

using std::cin;
using std::cout;
using std::string;
using ll = long long;

ll dp[20], ten[20]; // dp[i] 表示 i 个 0 到 i 个 9 中 0 的个数,dp[3] 表示 000~999 中 0 的个数
string s;
int len;
ll n;

ll dfs(int pos, bool prezero, bool limit) {
    // 先处理最普通的情况
    if (prezero == false and limit == false) {
        return dp[len-pos];
    } 
    if (pos == len-1) {
        if (prezero) return 0;
        else return 1;
    }
    ll res = 0;
    if (prezero) {
        // 1. 下一位继续填 0
        res += dfs(pos+1, true, false);
        // 2. 下一位不填 0
        res += 9*dfs(pos+1, false, false);
    }
    if (limit) {
        // 前面填到了极限,考虑一下 n 的这一位是不是 0
        if (s[pos] == '0') {
            res += n%ten[len-pos-1]+1;
            res += dfs(pos+1, false, true);
        }
        else {
            // 填 0
            res += ten[len-pos-1];
            res += dfs(pos+1, false, false);
            // 填 1~9 中的非极限数字
            res += (s[pos]-'0'-1)*dfs(pos+1, false, false);
            // 填极限数字
            res += dfs(pos+1, false, true);
        }
    }
    return res;
}

int main() {
    cin >> s;
    len = s.size();
    
    if (len == 1) {
        puts("0");
        return 0;
    }
    
    ten[0] = 1;
    for (int i = 1; i <= len; ++i) {
        ten[i] = ten[i-1]*10;
        n = n*10 + (s[i-1]-'0');
        dp[i] = i*ten[i-1];
    }
    
    // 枚举 n 的最高位的取值
    ll ans = dfs(1, true, false);
    for (int i = 1; i <= s[0]-'0'; ++i) {
        if (i == s[0]-'0') ans += dfs(1, false, true);
        else ans += dfs(1, false, false);
    }
    
    cout << ans << '\n';
    
    return 0;
}