CF1476F 题解

分析

显然是一道极其优美的 dp 题。

定义 $f_i$ 表示前 $i$ 盏灯最多能将 $[1, f_i]$ 的区间全部照亮,转移显然可以分情况讨论:

第 $i$ 盏灯笼向右照

如果 $f_i-1\ge i$,即前 $i-1$ 盏灯笼已经能照亮第 $i$ 盏灯,则有:$f_i=\max\{f_{i-1},i+p_i\}$,否则前 $i-1$ 盏灯笼不能将第 $i$ 盏灯笼照亮,故第 $i$ 盏灯笼只能由之后的灯笼照亮,则有:$f_i=f_{i-1}$。

            if(f[i - 1] >= i)f[i] = max(f[i - 1], i + p[i]);
            else f[i] = f[i - 1]; // 考虑灯笼向右照的情况

第 $i$ 盏灯笼向左照

此时需要找到最小的 $j$ 满足 $f_j\ge i-p_i-1$,即前 $j$ 盏灯笼与第 $i$ 盏灯笼可以完全照亮 $[1,i)$。那么 $[j+1,i-1]$ 的部分必然向右照更优,可以考虑对 $[j+1,i-1]$ 这个区间进行二分,优化时间复杂度。则有 :$f_i=\max(i-1,\underset{k\in[j+1,i-1]} {\max}\{k+p_k\})$。查找最大值可以使用 st 表维护静态区间最大值进行优化。

            int left = 0, right = i - 1, mid, res = -1;
            while(left <= right){ // 二分
                mid = left + right >> 1;
                if(f[mid] >= i - p[i] - 1){res = mid;right = mid - 1;} // 往左边寻找更小的 j
                else left = mid + 1; // 如果左边没有就往右边寻找
            }
            if(res != -1)f[i] = max(f[i], max((i - 1), query(res + 1, i - 1))); // 考虑灯笼向左照的情况

总的状态转移方程为:$$ f_i = \begin{cases} \max\{f_{i-1},i+p_i\}, & f_i-1\ge i \\ f_{i-1}, & f_i-1<i \\ \end{cases} $$

$$ f_i=\max(i-1,\underset{k\in[j+1,i-1]} {\max}\{k+p_k\}) $$

暴力转移时间复杂度为 $O(n^3)$,二分可以优化到 $O(n^2\log n)$, st 表维护区间静态最值可以进一步优化到 $O(n\log n)$,数据范围 $2\le n\le 3\times 10^5$,$O(n\log n)$ 可以通过本题。本题输入输出量较大,建议使用读入优化。以下是读入优化模板:

int read(){ // 读入优化
    int t = 1, x = 0;char ch = getchar(); // t 用于记录读入数字的符号,x 用于记录数字的绝对值
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();} // 忽略空格部分以及特判负号
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();} //读入数字
    return x * t; //特判负号
}

推导具体方案

如果 $f_i\ge n$ 则说明存在合法方案符合题意。输出具体方案时递归求解即可。

void getans(int pos){ // 递归推出具体方案
    if(pos == 0)return; // 边界
    if(f[pos] == f[pos - 1]){
        ans[pos] = 'R';getans(pos - 1);return; // 向右照
    }
    if(f[pos] == pos + p[pos] && f[pos - 1] >= pos){
        ans[pos] = 'R';getans(pos - 1);return; // 向右照
    }
    int left = 0, right = pos - 1, mid, res;
    while(left <= right){ // 二分
        mid = left + right >> 1;
        if(f[mid] >= pos - p[pos] - 1){ // 往左边寻找更小的 j
            res = mid;right = mid - 1;
        }
        else left = mid + 1; // 如果左边没有就往右边寻找
    }
    for(int i = res + 1 ; i < pos ; i ++)ans[i] = 'R'; // 向右照
    ans[pos] = 'L';getans(res); // 向左照
}

代码

#include <iostream> // 头文件不解释
#define MAXN 300005 // n 的最大值
using namespace std; // 命名空间不解释
int T, n; // 数据组数
int p[MAXN], f[MAXN]; // 每个灯笼亮度以及 dp 数组
int lg[MAXN], st[MAXN][25];  // st 表维护静态区间最大值
char ans[MAXN]; // 记录答案
int read(){ // 读入优化
    int t = 1, x = 0;char ch = getchar(); // t 用于记录读入数字的符号,x 用于记录数字的绝对值
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();} // 忽略空格部分以及特判负号
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();} //读入数字
    return x * t; //特判负号
}
int query(int l, int r){ // 查询 st 表
    if(l > r)return 0; // 特判
    int op = lg[r - l + 1];
    return max(st[l][op], st[r - (1 << op) + 1][op]);
}
void getans(int pos){ // 递归推出具体方案
    if(pos == 0)return; // 边界
    if(f[pos] == f[pos - 1]){ans[pos] = 'R';getans(pos - 1);return;} // 向右照
    if(f[pos] == pos + p[pos] && f[pos - 1] >= pos){ans[pos] = 'R';getans(pos - 1);return;} // 向右照
    int left = 0, right = pos - 1, mid, res;
    while(left <= right){ // 二分
        mid = left + right >> 1;
        if(f[mid] >= pos - p[pos] - 1){res = mid;right = mid - 1;} // 往左边寻找更小的 j
        else left = mid + 1; // 如果左边没有就往右边寻找
    }
    for(int i = res + 1 ; i < pos ; i ++)ans[i] = 'R'; // 向右照
    ans[pos] = 'L';getans(res); // 向左照
}
int main(){
    for(int i = 2 ; i < MAXN ; i ++)lg[i] = lg[i >> 1] + 1; // 预处理倍增数组
    T = read(); // 数据组数
    while(T--){  // 循环处理每组数据
        n = read(); // 读入个数
        for(int i = 1 ; i <= n ; i ++)p[i] = read(); // 读入亮度
        for(int i = 1 ; i <= n ; i ++)st[i][0] = i + p[i];
        for(int i = 1 ; i <= lg[n] ; i ++)
            for(int j = 1 ; j <= n - (1 << i) + 1 ; j ++)
                st[j][i] = max(st[j][i - 1], st[j + (1 << (i - 1))][i - 1]); // 预处理 st 表
        for(int i = 1 ; i <= n ; i ++){
            if(f[i - 1] >= i)f[i] = max(f[i - 1], i + p[i]);
            else f[i] = f[i - 1]; // 考虑灯笼向右照的情况
            int left = 0, right = i - 1, mid, res = -1;
            while(left <= right){ // 二分
                mid = left + right >> 1;
                if(f[mid] >= i - p[i] - 1){res = mid;right = mid - 1;} // 往左边寻找更小的 j
                else left = mid + 1; // 如果左边没有就往右边寻找
            }
            if(res != -1)f[i] = max(f[i], max(i - 1, query(res + 1, i - 1))); // 考虑灯笼向左照的情况
        }
        if(f[n] >= n){ // 判断是否存在合法方案
            puts("YES");getans(n); // 存在合法方案输出 YES 并求出具体方案
            for(int i = 1 ; i <= n ; i ++)putchar(ans[i]); // 输出方案
            putchar('\n');
        }
        else puts("NO"); // 不存在合法方案直接输出 NO
    }
    return 0;
} // 完结撒花~
posted @ 2023-08-14 12:33  tsqtsqtsq  阅读(45)  评论(0)    收藏  举报  来源