21-22寒假一期训练3

传送门:https://codeforces.com/group/uVAsoW2Jkj/contest/364973

原题链接:https://codeforces.com/gym/102916

J. Lost Island

题目大意

一个岛上有 \(n\) 种不同眼睛颜色的人,每种颜色有 \(a_i\) 个人

他们不知道自己的眼睛颜色,只知道别人的。一旦他们知道自己的眼睛颜色,就会在下一天里自杀

在第0天,一位旅行者告诉他们每种颜色 \(i\) 至少有 \(b_i\) 个人

现在告诉你所有的 \(a_i\)\(b_i\) ,求自杀发生的最后一天和自杀的总人数

分析规律

\(a_i\, = 1\)\(b_i = 1\),那个人没发现别人有第 \(i\) 种颜色,所以知道自己是第 \(i\) 种,第 \(1\) 天自杀

\(a_i\, = 2\)\(b_i = 1\),颜色 \(i\) 的人看到有一个人是颜色 \(i\) ,等着他在第一天自杀,但那个人没有自杀,所以他知道自己是第 \(i\) 种,第 \(2\) 天自杀

\(a_i\, = 2\)\(b_i = 2\),颜色 \(i\) 的人只看到有一个人是颜色 \(i\),所以知道自己是,第 \(1\) 天自杀

数学归纳法分析得,当 \(b_i > 0\) 时,颜色 \(i\) 的人全在 \(a - b + 1\) 天自杀

当只有一种 \(b_i = 0\) 时,他们发现其他颜色的人都挂掉后,就知道自己的颜色,遂在下一天自杀

当只有两种或以上 \(b_i = 0\) 时,他们可以活下来

特殊情况

当没有 \(b_i = 0\) 的颜色时,最后一批自杀的人是 \(b_i > 0\) ,他们看到其他颜色全挂掉后,就知道自己的颜色,所以在下一天就马上自杀了

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
typedef pair<int, int> pii;

const int N = 100010;

signed main()
{
    int n; cin >> n;
    int deadTot = 0, zeroCnt = 0, zeroTot = 0, lastDay = 0, second = 0;

    for(int i = 1; i <= n; i++)
    {
        int a, b;
        cin >> a >> b;
        if(b == 0) zeroCnt++, zeroTot += a;
        else
        {
            if(a - b + 1 > lastDay)
            {
                second = lastDay;	// 记录倒数第二久的天数
                lastDay = a - b + 1;
            }
            else second = max(second, a - b + 1);
            deadTot += a;
        }
    }
    if(zeroCnt == 1) deadTot += zeroTot, lastDay++;	
    if(zeroCnt == 0) lastDay = min(lastDay, second + 1);

    cout << lastDay << " " << deadTot << endl;

    return 0;
}

K. Bloodseeker

题目大意

\(n\) 个敌人,每个敌人需要打 \(t_i\) 下,打完会回复玩家 \(h_i\)

玩家一开始有 \(m\) 滴血,而且最多只能有 \(m\) 滴血

每打敌人一下就会消耗 \(1\) 滴血,如果玩家血量少于 \(0\) 就会结束游戏

在任意时刻,玩家可以选择打任意一个敌人

问玩家是否能打完所有敌人

分析规律

因为最多只有 \(m\) 滴血,所以一定要对 \(h_i\)\(min\,(\,h_i,\,m\,)\) 再进行后续操作

有两种敌人,一种是 \(h_i\,>=\,t_i\),打完补血;一种是 \(h_i\,<\,t_i\) 打完扣血

转换思路(思维题

对于打完补血的敌人,先把它们都打到剩 \(1\) 滴血,再去打别人。当打别人打到自己只剩 \(1\) 滴血时,再打补血怪,这样就能一滴也不浪费,补完所有的 \(h_i\)。所以在这种策略下,\(hp\,=\,m\, + \, \sum h_i\)。因为 \(h_i\,<=\,m\) 所以补血怪一定有 \(t_i\, <= m\),所以补血怪都能打

对于打完扣血的敌人,一个一个打,至于先打哪一个,列出方程贪心找规律(相似题目:AcWing 125. 耍杂技的牛

打两个敌人时:\(hp\, - t_i\, + \, h_i \, - \, t_{i + 1}\, + \, h_{i + 1}\)

两个可能会死的地方:

  • \(hp\, - t_i\)
  • \(hp\, - t_i\, + \, h_i \, - \, t_{i + 1}\)

对于第一个地方,因为打的都是扣血怪,所以无论先打 \(i\) 还是先打 \(i+1\),只要有一个 \(t>hp\) 就会去世

对于第二个地方,先打 \(h\) 更大的回的血多一点不容易去世,所以先打 \(h\) 更大

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
#define iofast ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
typedef pair<int, int> pii;

const int N = 200010;

pii ary[N];

signed main()
{
    int time; cin >> time;
    while(time--)
    {
        int n, m; cin >> n >> m;
        bool flag = true;
        int cnt = 1, hp = m;
        for(int i = 1; i <= n; i++)
        {
            int t, h; cin >> t >> h;
            h = min(h, m);  // 实际能补的血
            if(h - t >= 0) hp += h - t;
            else ary[cnt++] = {h, t};
        }
        // 一个一个打那些打完扣血的
        // 根据 h 排序,先打 h 大的
        sort(ary + 1, ary + 1 + cnt - 1);
        for(int i = cnt - 1; i >= 1; i--)
        {
            hp -= ary[i].second;
            if(hp < 0)
            {
                flag = false;
                break;
            }
            hp += ary[i].first;
        }
        if(flag) cout << "YES" << endl;
        else cout << "NO" << endl;
    }
}

L. Not the Longest Increasing Subsequence

题目描述

给定长度 \(n\) 的数组,数组元素大小在 \(1\)\(k\) 之间 \((1≤n≤10^6,1≤k≤n)\)

问最少删除多少个元素,使得数组中的 \(LIS\) 长度都小于 \(k\)

输出最少删除的个数和要删除元素的下标

分析规律

  • 因为元素大小在 \(1\)\(k\) 之间,所以长度为 \(k\)\(LIS\) 一定是 \(1, 2, ..., k\)
  • 要删除长度为 \(k\)\(LIS\) 有两种选择:
    1. 删除第 \(k\) 个数
    2. 删除前面的数,使得不存在长度为 \(k - 1\)\(LIS\)

分析得,该题为需要记录状态线性DP

状态表示:\(f\,[\,i\,]\) 为删除长度为 \(i\)\(LIS\) 需要的最小次数

状态转移方程:\(f\,[\,i\,] = min(\,f\,[\,i\,] + 1, \,f\,[\,i\,])\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
typedef pair<int, int> pii;
const int N = 1000100;

int ary[N];
int f[N], g[N];

signed main()
{
    int n, k; cin >> n >> k;
    for(int i = 1; i <= n; i++) cin >> ary[i];

    for(int i = 1; i <= n; i++)
    {
        int v = ary[i];
        g[i] = f[v];  // 记录状态转移过程

        if(v == 1) f[v]++;  // 边界条件
        // 可能是删除当前数,或者删除LIS前面的数
        else f[v] = min(f[v] + 1, f[v - 1]);
    }

    cout << f[k] << endl;
    
    // 从尾开始扫描,逆着查询状态转移的过程
    int cur = k;  // 当前要消除的LIS的长度
    for(int i = n; i >= 1; i--)
    {
        int v = ary[i];
        if(v == cur)
        {
            if(f[v] == g[i] + 1)  // 当前数被删除(删龙头)
                cout << i << " ";
            else    // 消除更短的LIS(删龙身)
                cur--;
        }
        f[v] = g[i];  // 往前恢复状态
    }
}

M. Binary Search Tree

题目大意

给定一颗由 \(n\) 个点组成的无根树 \((1≤n≤500000)\)

请你求出能作为二叉搜索树根节点的点,若没有这样的点则输出 -1

分析规律

若有拥有 4 条边或以上的点则谁都不能作为BST根节点,拥有3条边的点一定不是根节点。

若一个点可以作为BST根节点,则它的子树肯定是BST,而且它本身包括合法子树的数值范围一定是 \([1, \,n]\)

要得到每个点作为根节点可以表示的数值范围,就必须得到它每一条边上的子树的数值范围

时间不能是 \(O(n^2)\) ,所以必须整棵树一起遍历,不能对于某个节点遍历整棵树

解题

选定一个边小于3的点作为初节点,使用两次 dfs 。

第一次 dfs 求出从上到下的方向上,每个点及其子树作为BST的数值范围(若不能构成BST,则无表示范围)

第二次 dfs 依旧从上到下遍历,求两个值

  • 每个点的BST子树为遍历过程的父节点,求出逆方向的数值范围
  • 求每个点作为根节点表示的数值范围(此时每个点以各个方向为子树的数值范围已经求出)

(注意:输入较大开 iosfast)

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define double long double
#define iosfast ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
typedef pair<int, int> pii;

const int N = 1000010;

int h[N], e[N], ne[N], idx, dg[N];
// dp[i][j]表示以 j 为父节点的方向上,i节点包含数的范围
unordered_map<int, pii> dp[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

pii merge(int x, const vector<int>& child)
{
    if(child.empty()) return make_pair(x, x);
    if(child.size() == 1)
    {
        auto v = dp[child[0]][x];
        if(x <= v.first) return make_pair(x, v.second);
        else if(x >= v.second) return make_pair(v.first, x);
        else return make_pair(-INT32_MAX, INT32_MAX);
    }
    else
    {
        auto lc = dp[child[0]][x], rc = dp[child[1]][x];
        if(lc.first > rc.first) swap(lc, rc);
        if(lc.second <= x && x <= rc.first) 
            return make_pair(lc.first, rc.second);
        else
            return make_pair(-INT32_MAX, INT32_MAX);
    }
}

void dfs1(int x, int fa)
{
    vector<int> child;
    for(int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
        child.push_back(j);
        dfs1(j, x);   // 后序遍历,先访问完子节点,再操作当前节点
    }
    // 以 x 节点为父节点(fa方向),可以表示的范围
    // 即合并 x 的值和(x为父节点时)子节点可表示的范围
    dp[x][fa] = merge(x, child);
}

void dfs2(int x, int fa)
{
    vector<int> child;
    for(int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
        child.push_back(j);
    }
    /*
    *   对于每个节点做两件事
    * 1. 计算逆方向时,节点 x 的子节点能表示的范围,dp[child][x]
    * 2. 计算以 x 为根节点能表示的范围,dp[x][x]
    */
    if(child.empty())
        dp[x][x] = fa == x ? make_pair(x, x) : merge(x, {fa});
    else if(child.size() == 1)
    {
        if(fa == x)
            dp[x][child[0]] = make_pair(x, x);
        else
        {
            dp[x][child[0]] = merge(x, {fa});
            dp[x][x] = merge(x, {child[0], fa});
        }
    }
    else if(child.size() == 2)
    {
        if(fa == x)
        {
            dp[x][child[0]] = merge(x, {child[1]});
            dp[x][child[1]] = merge(x, {child[0]});
        }
        else
        {
            dp[x][child[0]] = merge(x, {child[1], fa});
            dp[x][child[1]] = merge(x, {child[0], fa});
        }
    }
    // 前序遍历
    for(auto v : child)
        dfs2(v, x);
}

signed main()
{
    iosfast;
    memset(h, -1, sizeof h);
    int n; cin >> n;
    for(int i = 1; i <= n - 1; i++)
    {
        int a, b; cin >> a >> b;
        add(a, b), add(b, a);
        dg[a]++, dg[b]++;
    }

    int rt = -1;
    for(int i = 1; i <= n; i++)
    {
        if(dg[i] >= 4)
        {
            cout << -1 << endl;
            return 0;
        }
        if(dg[i] <= 2 && rt == -1) rt = i;
    }

    dfs1(rt, rt);   // 计算一个方向下每个点的表示范围
    dfs2(rt, rt);   // 记录反方向每个点的表示范围

    vector<int> ans;
    for(int i = 1; i <= n; i++)
        if(dp[i][i] == make_pair((int)1, n))
            ans.push_back(i);
    
    if(ans.empty()) cout << -1 << endl;
    else
    {
        for(auto v : ans)
            cout << v << " ";
        cout << endl;
    }

    return 0;
}
posted @ 2022-01-22 11:25  攻城熊  阅读(47)  评论(0)    收藏  举报