educational round 1796 D & E 解题报告

Part1

D

【题意】

有一个数组 \(\{a_n\}\),给定 \(x,k\),现在要将数组内 \(k\) 个数加上 \(x\),其他的数减去 \(x\)。问得到的所有可能数组中最大子段和的最大值。

\(1 \le n \le 2 \times 10^5, 0 \le k \le \min(20, n), -10^9 \le x, a_i \le 10^9\)

【分析】
场上看到了 \(20\),感觉是个很关键的性质,想到先给所有数减去 \(x\),然后只有 \(20\) 个位置变动。然后由于最大子段和是 dp,我也就往 dp 上顺着想。于是就想出来了。但是思路不够清晰,调了很久。

\(dp_{i, j}\) 表示用了 \(i\)\(2x\)\([1, j]\) 最大后缀和。
我们的 dp 转移:
\(dp_{i, j} = \max(dp_{i-1, j - 1}+a_i + 2x, dp_{i, j-1} +a_i, 0)\)

最后的答案就是 \(\max \limits_{i = 1} ^ n \max \limits_{j = \max(0, k - n + i)} ^ k dp_{i, j}\)

注意边界,对于 \(i < j\) 的位置不合法

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e18;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr  << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
    string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; } 
    reverse(s.begin(), s.end()); cerr << s << endl; 
    return;
}
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
int dp[200100][30], tr[200100][30], tmp[200100][30];
int a[200100];int n,k,x;

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    //freopen();
    //freopen();
    //time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    int T; cin >> T;
    while(T--) {
        cin>>n>>k>>x;
        f(i,0,n)f(j,0,25)dp[i][j]=0;
        f(i,0,n)f(j,i+1,25)dp[i][j]=-inf;
        f(i,1,n){cin>>a[i]; a[i]-=x;}
        x=x+x;
        int ans=0;
        f(j,0,k){
            f(i,max(1ll, j),n){
                cmax(dp[i][j], max({dp[i-1][j] + a[i], (j-1>=0?dp[i-1][j-1] + a[i] + x:0ll), 0ll}));
                if(n - i + j >= k) cmax(ans, dp[i][j]);            
            }
        }
        cout<<ans<<endl;
    }
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
/*
2023/x/xx
start thinking at h:mm


start coding at h:mm
finish debugging at h:mm
*/

E

【题意】
给定一棵 \(n\) 点树,可以选定一个根,然后在上面选出一些深度不相同的点组成的链,这种方案的权值为这里面最短链的长度。求所有方案中,最大权值。

\(n \le 2 \times 10^5\)

【分析】
首先,这个剖分方式是“短链剖分”,而且是只能剖成不拐弯的链。但是我们二分答案可以有更好的判定。先考虑定下根了的情况。这种情况下,令 \(dp_i\) 表示该节点还需要往上加上多少个点才能达到长度要求。显然 \(dp_i = \max \limits_{j \in son_i} dp_j - 1\)。并且如果有两个节点的 \(dp\) 值都 \(>0\),这个根一定不可行。

考虑这种情况下可能可行的根其实是有限的。如果这个有两个不好的子节点的点不是根,那么只有这两个不好子节点末端可能是根,其他情况下这个点依然存在两个不好的子节点或者不优(很好证明,但是证明的时候可以发现是根的情况会有所不同)。如果是根也好办,当没有其他节点满足存在两个不好子节点的时候,从一个不好子节点出发,一定能将两个节点“展平”,而这是能做到的最好效果(体会一下,如果有其他不好的,选其他的可能修复该节点和根;如果没有,最多是修复根)。

另外一种情况是 \(dp_1 > 0\)。这时候只有使用 \(1\) 的最短链(肯定是它导致的)做根是最好的选择。

注意特殊情况判断。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr  << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
    string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; } 
    reverse(s.begin(), s.end()); cerr << s << endl; 
    return;
}
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
int mnl[200100];vector<int> t[200100]; int top[200100];
bool ok=0; int wt=0,ft=0;int n;
void dfs(int now,int fa,int tar) {
    int tmp=-inf; int num=0;
    for(int i : t[now]){
        if(i==fa)continue;
        dfs(i,now,tar);
        if(mnl[i]>0){
            num++;
            if(num>=2&&!ok){
                wt=now;ft=top[i];
            }
        }
        if(mnl[i]-1>tmp)top[now]=top[i];
        cmax(tmp,max(0ll, mnl[i]-1));
    }
    if(tmp==-inf)tmp=tar-1,top[now]=now;
    mnl[now]=tmp;
    if(num>=2){
        ok=1;
    }
}
bool ck(int mid){
    dfs(1,0,mid); 
    if(!ok&&mnl[1] <= 0){
        return 1;
    }
    else {
        if(mnl[1]>0)wt=1;
        int fft=ft;
        ok=0; 
        int dft=top[wt];
        dfs(dft,0,mid);
        if(!ok&&mnl[dft] <= 0)return 1;
        else {
            if(fft>n)return 0;
            ok=0; 
            dfs(fft,0,mid);
            if(!ok&&mnl[fft]<=0)return 1;
            else return 0;
        }
    }
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    //freopen();
    //freopen();
    //time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    int T; cin >> T;
    while(T--) {
        cin>>n;f(i,1,n)t[i].clear();
        f(i,1,n-1){int u,v;cin>>u>>v;t[u].push_back(v);t[v].push_back(u);}
        int l=1,r=n;while(l<r){
            int mid=(l+r+1)>>1;
            ok=0;
            if(ck(mid)) l=mid;
            else r=mid-1;
        }   
        cout<<l<<endl;
    }
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
/*
2023/x/xx
start thinking at h:mm


start coding at h:mm
finish debugging at h:mm
*/

Part2

D 有和 \(k\) 无关的贪心做法,E 有不用二分答案的线性的 dp 做法。

D:考虑分成加法和减法两个情况考虑。

对于加法,一定会加一段区间。然后最大子段和还有一种考虑方式那就是两个前缀和相减的最大值。显然对于一个右端点我们选择的是一个在它前面并且最小的左端点。那么加上 \(k\) 怎么做?考虑区间加 \(k\) 相当于对前缀和加等差数列。显然对于右端点 \(r\),一定选择 \([r-k, r-1]\) 这一段进行加等差数列操作。于是变成了维护这样一个东西:
image

这个东西可以用线段树维护,并且复杂度和 \(k\) 无关。

减法,就是 \(k := n-k, x := -x\) 的事情。

posted @ 2023-03-01 22:18  OIer某罗  阅读(15)  评论(0编辑  收藏  举报