A

签到。

B

签到。

C

一开始没有看懂题捏。 混合其实是或运算,给的 binary string 其实是第i种是危险的,由于n不大 所以其实暴力搜索即可。

bfs code

void solve()
{
    int n;
    cin >> n;
    string s;
    cin >> s;
 
    // danger[i] 表示状态i是否危险
    vector<int> danger(1 << n, 0);
    for (int i = 1; i < (1 << n); i++)
    {
        danger[i] = (s[i - 1] - '0');
    }
 
    // BFS
    queue<int> q;
    vector<bool> visited(1 << n, false);
 
    q.push(0); // 从空状态开始
    visited[0] = true;
 
    while (!q.empty())
    {
        int cur = q.front();
        q.pop();
 
        // 检查是否到达目标状态
        if (cur == (1 << n) - 1)
        {
            cout << "Yes";
            return;
        }
 
        // 尝试添加每种化学试剂
        for (int i = 0; i < n; i++)
        {
            if (cur & (1 << i))
            {
                continue;
            } // 已经包含这种试剂
 
            int next = cur | (1 << i); // 添加第i种试剂
 
            if (!visited[next] && !danger[next]) // 状态安全且未访问过
            {
                visited[next] = true;
                q.push(next);
            }
        }
    }
 
    cout << "No";
}

笔者写到这里,发现自己已经忘记bfs怎么写了… 于是紧急恶补了一下 现在大概搞懂了 原来细节这么多。

但是这样一来 就有优美的位运算版本了

void solve()
{
    int n;
    string s;
    cin >> n >> s;
 
    // dp[mask] 表示状态 mask 是否可达
    vector<int> dp(1 << n, 0);
    dp[0] = 1; // 空状态可达
 
    for (int mask = 0; mask < (1 << n); mask++)
    {
        if (!dp[mask])
        {
            continue;
        }
 
        // 尝试添加每种化学试剂
        for (int i = 0; i < n; i++)
        {
            if (mask & (1 << i))
            {
                // 已经包含这种试剂
                continue;
            }
 
            int next = mask | (1 << i);
            if (s[next - 1] == '0')
            {
                // 新状态安全
                dp[next] = 1;
            }
        }
    }
 
    cout << (dp[(1 << n) - 1] ? "Yes" : "No");
}

D

原问题等价于我们初始拥有 n 个空瓶,现在要最大化答案,而贡献答案的方式就是用多的空瓶 a 去交换比较少的空瓶 b。 也就等价于 我们每次花费 a - b 去贡献答案。 因此 自然可以想到关于 a - b 排序,这样一来我们只需在一次遍历中重复 贡献答案 的 操作 即可 代码如下:

void solve()
{
    int n, m;
    cin >> n >> m;
 
    vector<pair<int, int>> v(m);
    for (int i = 0; i < m; i++)
    {
        cin >> v[i].first >> v[i].second; // a, b
    }
 
    // 按 a - b 排序(损失从小到大)
    sort(all(v), [](auto x, auto y)
         { return x.first - x.second < y.first - y.second; });
 
    int ans = 0;
 
    for (auto [a, b] : v)
    {
 
        int loss = a - b; // 每一次贡献答案的代价
		while (n >= a)
		{
			ans++;
			n -= (a - b);
		}
    }
 
    cout << ans;
}

但是这样做存在超时风险 进而想到 其实可以通过除法优化

假设我们现在有 n 个空瓶 最多可以贡献 k 次答案( k 待求) 那么显然有:

那么 k 就满足:

上面这个式子真的对吗? 我们计算的这个 k 是满足 n >= a 的最优结果,但是其实 n >= a 时,我们仍然可以通过更多的一次操作 使 n < a 的同时贡献答案,所以最后要多加一次贡献。

void solve()
{
    int n, m;
    cin >> n >> m;
 
    vector<pair<int, int>> v(m);
    for (int i = 0; i < m; i++)
    {
        cin >> v[i].first >> v[i].second; // a, b
    }
 
    // 按 a-b 排序(损失从小到大)
    sort(all(v), [](auto x, auto y)
         { return x.first - x.second < y.first - y.second; });
 
    int ans = 0;
 
    for (auto [a, b] : v)
    {
 
        int loss = a - b;
 
        // 直接计算最多能交换多少次
        // 需要满足:n - k * loss >= a
        // 即:k <= (n - a) / loss
        if (n >= a)
        {
            int k = (n - a) / loss;
 
            if (k > 0)
            {
                ans += k;
                n -= k * loss;
            }
            if (n >= a)
            {
                ans++;
                n -= loss;
            }
        }
    }
 
    cout << ans;
}

E

到达 [i, j] 的时候应该是 第 i + j - 1 天,不妨记天数为 day ,最后拥有的费用是 dp[i][j] 这一天产生的开销是 v[day] 这个格子上获得的硬币是 a[i][j] 那么我们有:

而每一天的钱数都要大于0 这是一个从后往前推的过程: 比方说我今天有 x 元 一番操作之后明天变成了 -1 元 是不是说明我今天起码得有 x+1 元才能够得上这次消费?

所以变成了一次从终点往起点的dp

void solve()
{
    int h, w;
    cin >> h >> w;
 
    vector v(h, vector<int>(w));
    for (int i = 0; i < h; i++)
    {
        for (int j = 0; j < w; j++)
        {
            cin >> v[i][j];
        }
    }
 
    vector<int> p(h + w - 1);
    for (int i = 0; i < h + w - 1; i++)
    {
        cin >> p[i];
    }
 
    // dp[i][j] = 到达(i,j)时需要的最少硬币数
    vector dp(h, vector<int>(w, LLONG_MAX));
 
    // 从终点开始倒推
    dp[h - 1][w - 1] = max(0ll, p[h + w - 2] - v[h - 1][w - 1]);
 
    // 处理最后一行(只能从右边来)
    for (int j = w - 2; j >= 0; j--)
    {
        int day = (h - 1) + j;
        dp[h - 1][j] = max(0ll, dp[h - 1][j + 1] + p[day] - v[h - 1][j]);
    }
 
    // 处理最后一列(只能从下面来)
    for (int i = h - 2; i >= 0; i--)
    {
        int day = i + (w - 1);
        dp[i][w - 1] = max(0ll, dp[i + 1][w - 1] + p[day] - v[i][w - 1]);
    }
 
    // 处理其他位置
    for (int i = h - 2; i >= 0; i--)
    {
        for (int j = w - 2; j >= 0; j--)
        {
            int day = i + j;
            int need1 = max(0ll, dp[i + 1][j] + p[day] - v[i][j]); // 从下面来
            int need2 = max(0ll, dp[i][j + 1] + p[day] - v[i][j]); // 从右边来
            dp[i][j] = min(need1, need2);
        }
    }
 
    cout << dp[0][0];
}

写完后 笔者发现这道题其实完全可以二分猜一个起点 毕竟钱币是一直减少的 而我们dp的过程其实已经走完了全部可能。代码就不给了 总之就是把 dp 过程写进二分的check中 然后从开头往终点做dp 如果能到终点的话就可行

F

回到index