A.  小L的三角尺

本题主要内容:小 L 有 n 把直角三角尺,每把尺子的两条直角边分别为 x_i(不可打磨)和 y_i(可打磨)。
                         他可以选择一个非负整数 t_i 打磨第 i 把尺子的 y_i 边,使得打磨后长度变为 y_it_i,且 0≤t_i  y_i
                        所有尺子的打磨长度之和 \sum_{i=1}^{n} t_i不能超过总打磨额度 w
                        打磨后,每把尺子的斜边长度为 \sqrt{x_i^2 + (y_i - t_i)^2}(当t_i y_i 时,斜边长度为 x_i)。
                        现在需要在总打磨额度限制下,找到最优的打磨策略,使得所有尺子的斜边长度之和最小。

本题大致思路:使得所有尺子的斜边长度之和最小,就是把所有斜边求出总和再 减去 因为打磨所损耗的斜边的长度。
                         由于无法得知每个直角边怎么打磨才是最优的打磨方式,并且w并不是及其庞大,所以我们可以每次将打磨长度为 1 所损耗的斜边长度最大的那个三角形找到。
                         并重复上述操作,直至直角边无法继续打磨 或者 将要超过总打磨额度w
                         由于以上的找最大值的特性,我们考虑可以用优先队列的方式,并且每个三角形所存储的东西有点多,所以可以考虑用结构体来储存三角形的性质。
                         其中包括:打磨长度为 1 的直角边导致斜边所损耗的值,两个直角边,以及这个三角形已经被打磨了多少长度了。
                         最后按照 打磨长度为 1 的直角边导致斜边所损耗的值 进行排序,即可得出结果。

代码如下:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <queue>

using namespace std;
typedef long long LL;

const int N = 5e5 + 100;

LL n, w;

struct S
{
    double fu;      //打磨长度为 1 的直角边导致斜边所损耗的值
    double x;       //直角边x
    double y;       //直角边y
    double now;     //这个三角形已经被打磨了的长度

    bool operator<(const S& other) const  //使得优先队列为大根堆,用于排序
    {
        return fu < other.fu;
    }
};

int main() 
{
    scanf("%lld %lld", &n, &w);
    
    double all = 0;               // 所有尺子初始斜边长度之和
    
    priority_queue<S> q;        // 优先队列,按磨损长度降序排列
    
    for(int i = 1; i <= n; i++)
    {
        double x, y;
        scanf("%lf %lf", &x, &y);
        
        all += sqrt(x * x + y * y);     //计算初始斜边长度
        double fu = sqrt(x * x + y * y) - sqrt(x * x + (y-1) * (y-1));   //计算当前尺子斜边损耗长度
         
        q.push({fu, x, y, 0});  //当前尺子的初始状态入队:磨损长度 fu,已打磨量 now = 0
    }
    
    int use = 0;             // 已使用的打磨额度
    double res = 0;          // 已经减少的总长度
    
    while(use < w && !q.empty() )
    {
        auto t = q.top();   // 取出当前收益最大的尺子
        q.pop();
        
        if(t.fu <= 0) break;  //如果收益 <= 0,则继续打磨不会减少总长,提前结束
        
        // 打磨这一尺子
        t.now += 1;      // 已打磨量加1
        res += t.fu;     // 累加减少的长度
        use++;            // 使用额度加1
        
        if(t.y > t.now)
        {
            // 当前打磨 t.now 单位后 再打磨1单位所损耗的斜边长
            double f = sqrt(t.x * t.x + (t.y-t.now) * (t.y-t.now)) 
                       - sqrt(t.x * t.x+(t.y - t.now - 1) * (t.y - t.now - 1));
            q.push({f, t.x, t.y, t.now});   //处理完后继续入队
        }
    }
    
    printf("%.12lf\n", all - res);
    
    return 0;
}

 

B.  小L的彩球

本题主要内容:小 L 把编号为 1 到 n 的  n个彩球分到左右两个盒子里,左盒放 x 个、右盒放 n-x 个,
                         相邻编号的彩球之间有连线,若两球分属不同盒子则连线外露;
                         已知有 t 条连线外露,需要计算满足条件的彩球放置方法数,结果对 998244353 取模,每组输出对应答案。

本题主要内容:我们假设 如果连线外露,就相当于分成两段,那么总共就会分成 t+1 段。
                         如果 以左侧盒子开头,那么左侧盒子有的段数  L1 应该是 ⌈\frac{t+1}{2}⌉ ,右侧盒子有的段数 R1 应该是⌊\frac{t+1}{2}⌋
                         接下来就是: 将 x 个 左侧盒子的球分配到 L 段中,每段至少1个,方案数为 \binom{L-1}{x-1} (隔板法,每段至少 1 个)
                                               n-x 个 右侧盒子的球分配到 R 段中,每段至少1个,方案数为 \binom{R-1}{x-1} (隔板法,每段至少 1 个)
                         最后两者相乘就是答案。

代码实现:
#include <iostream>
#include <algorithm>

using namespace std;
typedef long long LL;

const int N = 1e6 + 100;
const LL MOD = 998244353;

LL T, n, x, t;    
LL fact[N], infact[N];         // fact[i] 存储 i! % MOD, infact[i] 存储 i! 的逆元 % MOD

// 快速幂
LL qmi(LL a, LL b, LL p) 
{
    LL res = 1;
    while (b) 
    {
        if (b & 1) res = res * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}

// 组合数 C(a, b) % p
LL f(LL a, LL b, LL p) 
{
    return fact[a] * infact[b] % p * infact[a - b] % p;
}

int main() 
{
    scanf("%lld", &T);

    // 预处理阶乘和阶乘逆元
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i++) 
    {
        fact[i] = fact[i - 1] * i % MOD;            
        infact[i] = infact[i - 1] * qmi(i, MOD - 2, MOD) % MOD; // i! 的逆元 = (i-1)! 的逆元 * i 的逆元
    }

    while (T--) 
    {
        scanf("%lld %lld %lld", &n, &x, &t);

        // 特判 t = 0 的情况:此时所有球在同一盒子,要么全左要么全右
        if (t == 0) 
        {
            // 左边盒子有 x 个,右边有 n-x 个;当 x == n 时全左,x == 0 时全右
            if (x == n || x == 0)
                printf("1\n");
            else
                printf("0\n");
            continue;
        }

        // 露在外面的线数为 t,则段数 k = t + 1
        int k = t + 1;

        // 两种可能的颜色排列:
        // 1. 开头在左侧
        // 2. 开头在右侧
        int l1 = (k + 1) / 2;   // 第一种情况中 L 段数
        int r1 = k / 2;         // 第一种情况中 R 段数
        int l2 = k / 2;         // 第二种情况中 L 段数
        int r2 = (k + 1) / 2;   // 第二种情况中 R 段数

        LL res = 0;

        // 第一种情况:以 L 开头,需要 x >= l1 且 n-x >= r1 才能每段至少一个球
        if (x >= l1 && n - x >= r1) 
            res = (res + f(x - 1, l1 - 1, MOD) * f(n - x - 1, r1 - 1, MOD)) % MOD;

        // 第二种情况:以 R 开头,需要 x >= l2 且 n-x >= r2
        if (x >= l2 && n - x >= r2)
            res = (res + f(x - 1, l2 - 1, MOD) * f(n - x - 1, r2 - 1, MOD)) % MOD;

        printf("%lld\n", res);
    }

    return 0;
}


D.  小L的扩展

本题主要内容:在一张 n 行 m 列的方格纸上,初始时有 a 个黑格和 b 个蓝格,黑格会在每个时间单位向四个方向扩散一格,
                        但蓝格在其变为白色的时刻 t_j之前无法被黑格扩散覆盖;每个时刻 t 的处理顺序是:
                        先让所有 t_j = t 的蓝格变白,再让黑格扩散,若扩散到的白格则会被染黑;需要求出整张纸所有方格都被染黑的时间。

本题主要思路:我首先想到的就是多源BFS,将黑色的格子都入队,然后找其他格子,每个时间单位会向外 扩展距离为1的其他格子。
                          但是由于有蓝色的格子,由于蓝色格子可能引入等待,导致扩散不是按层同步进行,普通BFS(队列)无法保证每个格子第一次被访问就是最早时间。
                          使用最小堆保证每次取出当前时间最小的格子,从而像Dijkstra一样正确更新。
                          黑色每单位时间向四邻域扩散一格。如果邻格是白色的,直接 当前时间+1。
                         如果邻格是蓝色,必须等到它变白后才能被染黑,因此到达时间为 max(当前时间+1, 蓝色变白时间)。

代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>

using namespace std;

typedef pair<long long, long long> PLL;
typedef long long LL;

const int N = 1e6 + 100;

int n, m, a, b;
LL g[N];                   // 存储每个格子的初始状态:0=初始黑,正数=蓝色及其变白时间,-1=普通白
LL dist[N];                // 记录每个格子被染黑的最早时间,-1表示尚未被染黑(或未更新)
priority_queue<PLL, vector<PLL>, greater<PLL>> q;   // 小根堆,按时间从小到大处理


void bfs()
{
    // 四个方向的位移:上(-m)、下(m)、左(-1)、右(1) (注意一维存储时,列数为 m)
    int dx[] = {-m, m, -1, 1};
    memset(dist, -1, sizeof(dist));   // 初始化距离为 -1

    // 将所有初始黑格(g[i]==0)入队,时间为 0
    for (int i = 1; i <= m * n; i++)
    {
        if (g[i] == 0)
        {
            dist[i] = 0;
            q.push({0, i});
        }
    }

    while (!q.empty())
    {
        auto t = q.top();    // 取出当前时间最小的格子
        q.pop();

        LL curTime = t.first;
        LL curPos  = t.second;

        // 将一维坐标转换为行列,用于边界判断
        int row = (curPos - 1) / m + 1;
        int col = (curPos - 1) % m + 1;

        // 尝试向四个方向扩散
        for (int i = 0; i < 4; i++)
        {
            // 边界检查:防止越界
            if (col == 1 && dx[i] == -1) continue;      // 最左列不能向左
            if (col == m && dx[i] == 1)  continue;      // 最右列不能向右
            if (row == 1 && dx[i] == -m) continue;      // 第一行不能向上
            if (row == n && dx[i] == m)  continue;      // 最后一行不能向下

            int nxtPos = curPos + dx[i];                 // 邻居的一维坐标
            LL nxtTime;                                   // 邻居可能被染黑的时间

            // 根据邻居的类型计算其被染黑的时间
            if (g[nxtPos] == -1)                         // 普通白色格子
                nxtTime = curTime + 1;
            else if (g[nxtPos] > 0)                      // 蓝色格子:需要等它变白
                nxtTime = max(curTime + 1, g[nxtPos]);   // 取扩散时间和变白时间的较大者
            else                                          // g[nxtPos]==0 是初始黑格,不会作为未染黑格子被更新,直接跳过
                continue;

            // 如果邻居尚未被染黑,或者找到了更早的时间,则更新并加入队列
            if (dist[nxtPos] == -1 || nxtTime < dist[nxtPos])
            {
                dist[nxtPos] = nxtTime;
                q.push({nxtTime, nxtPos});
            }
        }
    }
}

int main()
{
    scanf("%d %d %d %d", &n, &m, &a, &b);

    // 初始化所有格子为普通白色(-1)
    memset(g, -1, sizeof(g));

    // 读入 a 个初始黑格,标记为 0
    for (int i = 1; i <= a; i++)
    {
        int x, y;
        scanf("%d %d", &x, &y);
        g[(x - 1) * m + y] = 0;          // 将二维坐标映射到一维
    }

    // 读入 b 个蓝色格子,标记为它们的变白时间 t
    for (int i = 1; i <= b; i++)
    {
        LL x, y, t;
        scanf("%lld %lld %lld", &x, &y, &t);
        g[(x - 1) * m + y] = t;           // 存储变白时间
    }

    bfs();   // 执行扩散计算

    // 找出所有格子被染黑时间的最大值,即整个网格完全变黑的时间
    LL ma = 0;
    for (int i = 1; i <= n * m; i++)
        if (dist[i] != -1)
            ma = max(ma, dist[i]);

    printf("%lld\n", ma);
    return 0;
}

E.  小L的空投

本题主要内容:A 国共有 n 个城市和 m 条双向公路,每个城市有一个高度 h_i。在接下来 x 天里,每天水位会上升到 H_i(严格递增),
                         所有高度 ≤ H_i的城市会被淹没,与其直接相连的公路也不可行。
                         对于每一天,需要统计所有未被淹没的城市中,连通分量大小 ≥ d 的数量,这个数量就是当天需要发放的空投总数。

本题主要思路:使用倒序处理对于当前水位  H_i,我们需要将所有高度大于 H_i 的城市加入到连通图中。
                         由于我们是从高水位向低水位处理,所以每次要加入的城市就是那些高度比当前水位更高的城市。
                         每次加入完成后,就维护连通块。当加入城市 u 时,首先检查是否是独立的连通块,或者是否与其他连通块中的某点相邻,进行合并。


代码实现:
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int,int> PII;

const int N = 5e5 + 100;

int n,m,x,d;
int h[N];            // 每个城市的高度
int p[N], s[N];      // 并查集父节点和集合大小
int cnt = 0;         // 当前满足大小≥d的连通块个数
vector<int> adj[N];  // 邻接表
vector<int> H;       // 每天的水位


int find(int x) 
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 合并两个城市所在的集合,并更新cnt
void unite(int a, int b) 
{
    int ra = find(a), rb = find(b);
    if(ra == rb) return;

    //更新cnt:减去原来两个可能有的贡献
    if(s[ra] >= d) cnt--;
    if(s[rb] >= d) cnt--;
    
    // 按大小合并,保证ra是较大的集合
    if(s[ra] < s[rb]) swap(ra, rb);
    p[rb] = ra;
    s[ra] += s[rb];

    // 更新cnt:加上新集合可能的贡献
    if(s[ra] >= d) cnt++;
}

int main() 
{
    scanf("%d %d %d %d", &n, &m, &x, &d);
    for(int i = 1; i <= n; i++) scanf("%d", &h[i]);

    // 建图
    for(int i = 0; i < m; i++) 
    {
        int u, v;
        scanf("%d %d", &u, &v);
        adj[u].push_back(v);
        adj[v].push_back(u);
    }


    for(int i = 0; i < x; i++) 
    {
        int tmp;
        scanf("%d", &tmp);
        H.push_back(tmp);
    }

    // 将城市按高度从大到小排序加
    vector<PII> c;  // (高度, 城市编号)
    for(int i = 1; i <= n; i++) 
        c.push_back(h[i], i);
    sort(c.begin(), c.end(), greater<PII>());  // 降序

    int idx = 0;          // 指向c中下一个待加入的城市
    int a[x + 1];         // 存储每天答案(下标0对应第1天)

    // 倒序遍历每一天
    for(int i = x - 1; i >= 0; i--) 
    {
        int now = H[i];   // 当前天的水位

        // 将所有高度 > now 的城市加入并查集(这些城市在这一天未被淹没)
        while(idx < n && c[idx].first > now) 
        {
            int u = c[idx].second;   // 城市编号
            p[u] = u;                // 初始化并查集
            s[u] = 1;
            if(d <= 1) cnt++;        // 如果d=1,每个单独城市就是一个合法连通块

            // 尝试与已经加入的邻居合并
            for(int v : adj[u]) 
            {
                if(p[v] != 0)        // 邻居v已经被加入
                    unite(u, v);
            }
            idx++;
        }
        a[i] = cnt;   // 这一天的答案
    }

    for(int i = 0; i < x; i++)
        printf("%d\n", a[i]);

    return 0;
}


G.  小L的散步

本题主要内容:小 L 面前有 n 个石块,每个石块长度为 x_i,从位置 0 开始依次排列,相邻石块间有缝隙(最后一个石块的右端点也视为缝隙)。
                         他从位置 0 出发,每一步跨过长度 y_i,脚掌长度为 l,脚后跟位置即为当前位置。
                         需要判断:在初始位置以及每一步走完后的位置,他的脚掌是否覆盖了任何一个缝隙(脚掌边缘恰好落在缝隙上不算踩中)。
                         如果有,输出 YES,否则输出 NO。

本题大致思路:我么可以发现,对于石块的长度的前缀和,就是每个缝隙所在的位置。因此我们可以先计算x_i的前缀和,
                         然后依次遍历每走一步,计算脚后跟 l 和脚尖 r,判断是否存在某一缝隙满足:l < X <r 
                         如果存在就是YES,否则就是NO。

代码实现:
#include <iostream>
#include <algorithm>

using namespace std;
typedef long long LL;

const int N = 2e5 + 100;

LL n, m, r;
LL s[N], y[N];             // s[i] 为前 i 个石块的总长度(即第 i 个石块的右端点位置),y[i] 为第 i 步步长

int main() 
{
    scanf("%lld %lld %lld", &n, &m, &r);
    
    // 读入每个石块长度并计算前缀和
    for (int i = 1; i <= n; i++) 
    {
        scanf("%lld", &s[i]);
        s[i] += s[i - 1];
    }
    
    for (int i = 1; i <= m; i++) 
        scanf("%lld", &y[i]);
    
    LL l = 0;          // 当前脚后跟的位置,初始为 0
    LL cnt = 1;        // 指向当前可能踩到的第一个缝隙(即第 cnt 个石块的右端点)
    
    // 检查初始位置是否踩中缝隙
    if (s[cnt] > l && s[cnt] < r)   // 缝隙位于脚掌区间 (l, r) 内
    {
        printf("YES\n");
        return 0;
    }
    
    for (int i = 1; i <= m; i++)
    {
        l += y[i];      // 脚后跟向前移动 y[i]
        r += y[i];      // 脚掌右端点也相应移动(脚长不变,所以 r 是脚后跟 + 脚长)
        
        // 当脚后跟已经超过当前缝隙位置时,说明这个缝隙已经被跨过,需要检查下一个缝隙
        while (cnt <= n && l > s[cnt]) 
            cnt++;
        
        // 如果当前缝隙在脚掌区间内,则踩中
        if (cnt <= n && s[cnt] > l && s[cnt] < r) 
        {
            printf("YES\n");
            return 0;
        }
    }
    
    // 全程没有踩中缝隙
    printf("NO\n");
    
    return 0;
}

H.  小L的数组

本题主要内容:给定两个长度为 n 的数组 AB,初始值 x=0。需要依次进行 n 次操作,每次操作必须从以下两种规则中选择一种:
                         规则一:将 x 更新为  max( 0, x-a_i)
                         规则二:将 x 更新为 x ⊕ b_i
                         要求计算在执行完所有 n 次操作后,x 可能达到的最大值。

本题主要思路:由于数据范围并不大,所以我们可以把每次按不同规则所产生的数都记录下来,然后分别将这些数进行下一次的两种操作。
                         由于考虑到去重的问题,我们可以选择用 unordered_set 来记录。

代码实现:
#include <iostream>
#include <algorithm>
#include <unordered_set>

using namespace std;

const int N = 3000;

int n;
int a[N], b[N];

int main() 
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) 
        scanf("%d", &a[i]);
    for (int i = 0; i < n; i++) 
        scanf("%d", &b[i]);

    // 使用哈希集合存储当前所有可能的 x 值
    unordered_set<int> s;
    s.insert(0);          // 初始 x = 0

    // 依次进行 n 次操作
    for (int i = 0; i < n; i++) 
    {
        unordered_set<int> tmp;   // 临时集合,存放第 i 步后的所有可能值
        for (auto x : s) 
        {
            // 规则一:减去 a[i] 并保证非负
            tmp.insert(max(0, x - a[i]));
            // 规则二:与 b[i] 按位异或
            tmp.insert(x ^ b[i]);
        }
        // 将当前集合更新为临时集合
        s.swap(tmp);
    }

    // 找出所有可能结果中的最大值
    int res = -1e9;
    for (auto i : s) 
        res = max(res, i);
    
    printf("%d\n", res);
    return 0;
}

 

K.  小L的游戏1                  

本题主要内容:游戏在初始值为 0 的变量 x 上进行,小 L 和 falleaves01 轮流对 x 累加操作,小 L 先手。
                        每一轮分为两个回合:
                        小 L 的回合:x+=m
                        falleaves01 的回合:x+=n
                       一旦 x≥z,游戏立即结束。需要判断最后一次操作是谁执行的.

本题主要思路:经过两个回合,x增加的值就是 m+n ,因此我们可以先让最终的 z 对 m+n 取模
                         如果最后的余数是 0 ,就说明最后一次是 fallleaves01 操作的,输出1
                         如果最后的余数 大于0 小于等于 m就说明最后一次是 小 L操作的,输出0
                         如果最后的余数是  大于 m 小于 m+n ,就说明最后一次是 fallleaves01 操作的,输出1

代码实现:
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

LL t;       
LL m, n, z; 

int main() 
{
    scanf("%lld", &t);
    while (t--)
    {
        scanf("%lld %lld %lld", &m, &n, &z);
        
        // 每一轮两人共增加 m+n,所以用 z 对 (m+n) 取模得到最后一轮开始后还需增加的量
        LL res = z % (m + n);
        
        if (res == 0)
            // 余数为 0:经过整数轮后恰好达到 z,最后一轮的最后一次操作是对方加的 n
            printf("1");
        else if (res <= m)
            // 余数在 [1, m] 之间:最后一轮中小 L 加 m 后直接达到或超过 z,因此最后一次是小 L
            printf("0");
        else
            // 余数大于 m:小 L 加 m 后仍未达到,对方加 n 后才达到,最后一次是对方
            printf("1");
    }
    return 0;
}