ABC184 D——F && 一道LC好题

ABC184 D——F && 一道好题

D - increment of coins

问题描述

你的背包里已经有三种硬币,分别为 \(A\)\(B\)\(C\) 个。你将不断进行如下操作直到你拥有 \(100\) 个相同的硬币:

  • 从背包中随机抽出一枚硬币(所有的硬币材质相同,抽取的概率相同),并往背包中放入共两枚相同颜色的硬币

最后期望的操作次数是多少

Constraints

  • \(0 \le A,B,C \le 99\)
  • \(A + B + C \ge 1\)

思路分析

关键词:

  • 期望 —— 期望dp

好像也没有什么关键词,只是需要构建状态转移。

显然我们得从已知出发不断去递推位置,已知的是任何一种硬币的个数到了\(100\)时游戏都结束了。

不妨设 f[i][j][k] := 当第一类硬币有i个,第二类硬币有j个,第三个硬币有k个时,期望的操作次数

显然 f[100][*][*] = f[*][100][*] = f[*][*][100] = 0,其中 * 代表通配。

这也间接的说明我们需要自顶向下

状态转移式比较容易表示:

  • f[i][j][k] = (i * f[i+1][j][k] + j * f[i][j+1][k] + k * f[i][j][k+1]) / (i+j+k) + 1

用语言描述就是,当前硬币数量期望的操作数等于,每种硬币数量加一之后的期望操作数乘上该硬币加一的概率再加上一(这个一代表这一次操作)。

有一些拗口,仅做解释。

代码

#include <bits/stdc++.h>
using namespace std;
using db = double;
using vi = vector<int>;
using pii = pair<int, int>;
using pdd = pair<db, db>;
const int maxn = 1e2 + 5;
db f[105][105][105];


void solve(){
    int a, b, c;
    cin >> a >> b >> c;
    for (int i = 0; i <= 100; ++ i)
        for (int j = 0; j <= 100; ++ j)
            f[100][i][j] = f[i][100][j] = f[i][j][100] = 0;
    
    auto cal = [&](int i, int j, int k){
        db tot = i + j + k;
        return i / tot * f[i + 1][j][k] + j / tot * f[i][j + 1][k] + k / tot * f[i][j][k + 1];
    };

    for (int i = 99; ~i; -- i){
        for (int j = 99; ~j; -- j){
            for (int k = 99; ~k; -- k){
                f[i][j][k] = cal(i, j, k) + 1;          
            }
        }
    }
    cout << fixed << setprecision(9) << f[a][b][c] << '\n';
}

int main(){
    ios::sync_with_stdio(0), cin.tie(0);
    int t = 1; 
    // cin >> t;
    while (t--) solve();
    return 0;
}

E - Third Avenue

问题描述

给定一个 \(H\) 行, \(W\) 列的二维平面。

共存在如下几种点:

  • . 代表空地
  • a-z 代表传送位置
  • # 代表禁止访问的位置
  • S 代表开始位置, G 代表目的地

人物从S的位置出发,每次进行以下两种操作之一:

  1. 往相邻且不为#的位置走一步。
  2. 假如处于传送位置,可以传送到任何字母相同的位置

需要找到,**最少需要多少次操作能从SG ,假如不能输出 -1 **。

Constraints

  • \(1\le H,W \le 2000\)
  • \(a_{i,j}\) is S, G, ., #, or a lowercase English letter.
  • There is exactly one square represented as S and one square represented as G.

思路分析

关键词:

  • 最小操作次数 —— 最短路问题(dp | bfs | 双向bfs | ...)
  • 往相邻方向行走 —— bfs & dfs

题目可以转化为最短路问题,怎么在时间复杂度限制下将题目所给条件转化为找到SG之间的最短路

主要有两种思路:

  • dp ,假设我们设 dp[i][j] := 从位置(i,j)最小的操作次数

我们会发现,第一,我们的初始位置不在(0,0),且顺序dp是无法解决问题的,因此dp[i][j]的更新可能影响,dp[u][v] | u <= i, v <= j

  • bfs ,我们可以找到最开始的S,假如队列,每次进行两种操作:
    • 往四周走一走
    • 假如当前是字母,则(传送)访问所有的字母。

我们发现第二种方法更容易实现,且是正确的。因为只要点(i,j)访问过,由于bfs的特性,则其一定是最小值。

但是需要注意的是每个字母,我们只需要访问一次即可(因为最开始访问的永远是开销最小的!),假如我们每次都访问字母在最劣情况下,时间复杂度会变为 \(\mathcal{O(H^2\times W^2)}\) 显然是无法满足题目要求的。因此,我们可以设一个标记,每个字母只访问一次,这样就可以在 \(\mathcal{O(H\times W)}\) 的时限内完成!

代码

#include <bits/stdc++.h>
using namespace std;
#define DEBUG 0

#define all(x) x.begin(), x.end()
#define sz(x) (int(x.size()))
#define Case(x, y) ("Case #" + to_string(x) + ": " + to_string(y))
#define pb push_back
#define mp make_pair
#define fr(x) freopen(x, "r", stdin)
#define fw(x) freopen(x, "w", stdout)
using ll = long long;
using db = double;
using vi = vector<int>;
using vvi = vector<vi>;
using pii = pair<int, int>;



int c[2005][2005]; // 记录(i,j)的最小值
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
vector<pii> pos[30]; // 每个字母的位置
int used[30]; // 每个字母的访问情况
void solve(){
    int H, W; cin >> H >> W;
    vector<string> f(H);
    for (int i = 0; i < H; ++ i) cin >> f[i];
    memset(c, -1, sizeof(c));
    queue<pii> que;
    pii start, end;

    auto check = [&](pii x){
        return (f[x.first][x.second] - 'a' >= 0 && f[x.first][x.second] - 'a' < 26);
    };

    for (int i = 0; i < H; ++ i){
        for (int j = 0; j < W; ++ j){
            if (f[i][j] - 'a' >= 0 && f[i][j] - 'a' < 26) pos[f[i][j] - 'a'].pb(mp(i, j));
            if (f[i][j] == 'S') start = mp(i, j);
            if (f[i][j] == 'G') end = mp(i, j);
        }
    }
    c[start.first][start.second] = 0;
    que.push(start);
    while (!que.empty()){
        pii qf = que.front(); que.pop();
        for (int k = 0; k < 4; ++ k){
            int nx = qf.first + dx[k], ny = qf.second + dy[k];
            if (nx >= 0 && nx < H && ny >= 0 && ny < W && c[nx][ny] == -1 && f[nx][ny] != '#'){
                c[nx][ny] = c[qf.first][qf.second] + 1;
                que.push(mp(nx, ny));
            }
            if (check(qf) && !used[f[qf.first][qf.second] - 'a']){
                for (auto &e: pos[f[qf.first][qf.second] - 'a']){
                    if (c[e.first][e.second] == -1){
                        c[e.first][e.second] = c[qf.first][qf.second] + 1;
                        que.push(e);
                    }
                }
                used[f[qf.first][qf.second] - 'a'] = 1;
            }
        }
    }
    cout << c[end.first][end.second] << '\n';
}


int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    // fr("input.txt");
    int t = 1;
    //  cin >> t;
    while (t--) solve();
    // fclose(stdin);
    return 0;
}

F - Programming Contest

问题描述

给定 \(T\) 的限制时间, \(N\) 个问题,其中解决每个问题需要 \(A_i\) 的时间。你可以从中取任意数量的问题,要求解决这些题问题的总时间不能超过 \(T\) 。需要你寻找最长的解决选择问题需要花费的时间

(换言之,你需要在 \(N\) 个问题中选出一个组合,其总开销不超过 \(T\) ,且尽可能接近 \(T\))

Constraints

  • All values in input are integers.
  • \(1\le N\le 40\)
  • \(1\le T\le 10^9\)
  • \(1\le Ai\le 109\)

思路分析

关键词:

  • 最接近 —— 二分查找
  • \(N\) 个元素的组合 —— bitmask (状压)

这题直接秒了,实际上是一个折半搜索 & 二分查找

因为我们假如暴力的枚举每个状态需要 \(2^{40}\) 的时间复杂度,显然是不符合的。

但是,我们发现我们可以分别枚举一般的区间也就是两次 \(2^{20}\) 的枚举。

具体流程如下:

  1. 首先枚举 upper,也就是高\(20\)位,并将结果保存在 arr 数组中(注意开ll)
  2. 随后枚举 lower,也就是低\(20\)
  3. 每次枚举的过程中,计算当前总和tot,随后在arr数组中二分查找T - tot
  4. 不断更新答案即可。

这里有个小trick,由于我们需要寻找值的区间在\([0, T-tot]\)之间,而:

  • lower_bound 的搜索区间为 \([T-tot, \infty]\)
  • upper_bound 的搜索区间为 \((T-tot,\infty]\)

所以我们应该运用 upper_bound 再利用 prev 即可找到答案(比lower_bound少一次判断)

代码

#include <bits/stdc++.h>
using namespace std;
#define DEBUG 0

#define all(x) x.begin(), x.end()
#define sz(x) (int(x.size()))
#define Case(x, y) ("Case #" + to_string(x) + ": " + to_string(y))
#define pb push_back
#define mp make_pair
#define fr(x) freopen(x, "r", stdin)
#define fw(x) freopen(x, "w", stdout)
using ll = long long;
using db = double;
using vi = vector<int>;
using vl = vector<ll>;
using vvi = vector<vi>;
using pii = pair<int, int>;


void solve(){
    int N, T; cin >> N >> T;
    vi f(N);
    for (int i = 0; i < N; ++ i) cin >> f[i];
    vl arr; // 存储折半枚举的部分
    int low = N >> 1, upp = N - low;
    for (int mask = 0; mask < (1 << upp); ++ mask){
        ll tot = 0;
        for (int i = 0; i < 25; ++ i){
            if (mask & (1ll << i)) tot += f[i + low];
        }
        arr.pb(tot);
    }
    sort(all(arr));

    ll ans = 0;
    for (int mask = 0; mask < (1 << low); ++ mask){
        ll tot = 0;
        for (int i = 0; i < 25; ++ i){
            if (mask & (1ll << i)) tot += f[i];
        }
        // 这里用的 lower_bound,所以需要多做一次判断
        // 你来试试 upper_bound 的用法?!
        auto iter = lower_bound(all(arr), T - tot);
        if (iter == arr.begin()) continue;
        else if (iter != arr.end() && (*iter) == T - tot) ans = max(ans, tot + (*iter));
        else ans = max(ans, tot + (*prev(iter)));
    }
    cout << ans << '\n';
}


int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    // fr("input.txt");
    int t = 1;
    //  cin >> t;
    while (t--) solve();
    // fclose(stdin);
    return 0;
}

LC222. 完全二叉树的节点个数

问题描述

给出一个完全二叉树,求出该树的节点个数。

说明:

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 \(h\) 层,则该层包含 \(1\sim2h\) 个节点。

思路分析

这题,刚开始直接 dfs 就水过了,后面觉得不好意思还是去看了下题解,发现真的非常妙遂决定记录一下。

关键词:

  • 完全二叉树 —— bitmask 性质,及其树节点性质

直接说性质吧:

  1. 设完全二叉树有 \(h\;(0-index)\) 层,其中前 \(h-1\) 层结点全满,共 \(2^h-1\) 个结点

  2. 其中完全二叉树的深度 \(h\) 可由不断访问其左孩子获取。

  3. 对于完全二叉树中的一个结点,设其编号为 id 其满足如下性质:

    \[eg:id=12\\ id=1100_2\\ 除了首个1之外\\ 1 \rightarrow 代表从当前结点往右结点\\ 0 \rightarrow 代表从当前结点往左结点 \]

由于第 \(h\) 层的结点个数的可能区间为 \([1, 2^h]\)。因此我们可以得到如下算法:

  1. 找到最大深度 mxd
  2. 二分查找 mxd 层可能的结点数量。
  3. 利用结点对应的位运算规律,用 \(\mathcal{O(\log2^h)=\mathcal{O(h)}}\) 级的复杂度完成判断

因此,算法的时间复杂度由 dfs\(\mathcal{O(n)}\) 变为 \(\mathcal{O(h^2)}\) 其中 \(h\) 为树的高度,满足 \(O(h)=O(\log n)\)

代码

 /*
好题, bitmask & binary search 
主要是需要学会 在完全二叉树中 node = 1|xxxx 其中从大到小 x == 1 -> right, x == 0 --> left
 */
 class Solution {
public:
    int mxDep(TreeNode *bt){
        int mxd = 0;
        while (bt->left){
            ++ mxd;
            bt = bt->left;
        }
        return mxd;
    }

    int countNodes(TreeNode* root) {
        if (!root) return 0;
        int mxd = mxDep(root); // 最大深度

        auto check = [&](int x){
            TreeNode *bt = root;
            x += (1 << mxd) - 1;
            for (int bit = mxd - 1; ~bit; -- bit){
                if (x & (1 << bit)){
                    if (bt->right) bt = bt->right;
                    else return false;
                }else {
                    if (bt->left) bt = bt->left;
                    else return false;
                }
            }
            return true;
        };

        int lower = 1, upper = (1 << mxd) + 1;
        while (lower + 1 < upper){
            int mid = (lower + upper) >> 1;
            if (check(mid)) lower = mid;
            else upper = mid;
        }
        cout << mxd << " " << lower << '\n';
        return (1 << mxd) - 1 + lower;
    }
};

后记与小结

  • ABC184——D :
    • 遇到概率期望问题时,优先考虑 dp
    • 在状态推导的过程中,总是由简单的已知,不断由概率推向未知
  • ABC184——E:
    • 最短路,最小次数问题考虑: bfsdp ,判断能不能 dp ,一般在最短问题上只需要判断能否顺序 dp 就可以。
    • bfs 问题一般的时间复杂度只是访问整个二维平面的,bfs 问题需要记得打标记,避免无意义的访问
  • ABC——F:
    • 当问题有物品组合的意思,且N 较小时,可以考虑状态压缩
    • 当时间复杂度不能满足要求时,可以考虑把问题规模压缩并利用二分查找这个奇妙的算法进行时间复杂度优化,比如该题中的 折半查找
  • LC222
    • 完全二叉树的性质,最需要记忆的是:路径与位运算的关系,这样访问只需要 log 级复杂度

这一次的 ABC 大多数都会,还是回忆了一波 概率dp bfs求最短 bitmask 与折半搜索

posted @ 2020-11-24 15:29  Last_Whisper  阅读(221)  评论(0编辑  收藏  举报