NOIP2024集训 Day33 总结

前言

若巅峰不在,那就重踏来时之路。

今天是 DP 优化专题,感觉只要写出了暴力,剩下的部分都挺典的。

简称,暴力对了,就是有手就行。

怎么说,感觉今天状态不太好,老是细节上出现一些很逆天的错误。

例如:

for (auto i = dp.begin(); i != dp.end(); ++i)
{
    pair<ll, ll> j = *i;
    ans = j.first * n + j.second;//你ans这么求吗,为什么不是取max,MD调死了。
}

本质上来说,最后一道题我是没有过的,但是这个 DP 优化并不算太难了,但是我认为不能把时间浪费在去写这个东西上面,所以就先来写总结。

由于今天的题目质量感觉不算特别好(?,所以我就只写一部分有意义的题的题解。

Non-equal Neighbours

难评,为什么大家都会觉得这个题很简单,为什么大家都能想到容斥,为什么大家都觉得这个题容斥很好做,我破防了。

题做少了导致的。

我们观察到第二个要求,bibi+1 并不是一个很好转移的东西,所以我们考虑一个容斥。

定义满足 bi=bi+1有用的,那么最终通过容斥的答案就是:

  • 0 个有用的点的方案 1 个有用的点的方案 +2 个有用的点的方案..... 以此类推。

而实际上,这个容斥系数只跟这个有用的点的个数的奇偶性相关。

以及,本质上来说,对于一个至少有 k 个有用的点的方案,其实就是将序列分成 nk 段,每段内部都填上相同的数。

故我们考虑一个 dpi,0/1 表示填完前 i 个数,共有奇数/偶数段的方案数。

转移比较显然,枚举一个 j,让中间的数填成一样的,即 dpi,0/1=dpj,1/0×mink=j+1iak

优化的话考虑一个单调栈,每次用更小的弹掉队首,弹掉之后更新一下答案,加入的时候也更新一下答案,注意一下容斥系数即可。

细节见代码:

#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
#define int long long
#define maxn 200005
int a[maxn], dp[maxn][2], sum[maxn][2];
int stk[maxn], cnt = 0;
int p1(int x)
{
	return (x & 1) ? mod - 1 : 1;
}
signed main()
{
	int n;
	scanf("%lld", &n);
	for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);
	dp[0][0] = sum[0][0] = 1, dp[0][1] = sum[0][1] = 0;
	stk[0] = 0;
	for(int i = 1; i <= n; i++)
	{
		while(cnt && a[stk[cnt]] >= a[i]) cnt--;
		stk[++cnt] = i;
		dp[i][0] = ((cnt == 1 ? 0 : dp[stk[cnt - 1]][0]) + (sum[i - 1][1] - (cnt == 1 ? 0 : sum[stk[cnt - 1] - 1][1]) + mod) * a[i]) % mod;
		dp[i][1] = ((cnt == 1 ? 0 : dp[stk[cnt - 1]][1]) + (sum[i - 1][0] - (cnt == 1 ? 0 : sum[stk[cnt - 1] - 1][0]) + mod) * a[i]) % mod;
		sum[i][0] = (sum[i - 1][0] + dp[i][0]) % mod;
		sum[i][1] = (sum[i - 1][1] + dp[i][1]) % mod;
	}
	printf("%lld\n", (dp[n][0] - dp[n][1] + mod) * p1(n) % mod);
	return 0;
}

Mod Mod Mod

虽然是一个比较普通的有用的状态是有限的一个优化,但是现在看来还是觉得挺神奇的。

我们考虑一个朴素的 DP,即定义 dpi,j 表示最初的 xmoda1moda2..modai=j 的最大答案。

显然有一个转移:dpi+1,j%ai+1=max(dpi,j+j%ai+1)

我们考虑对这个 DP 打一个表。

首先对于 i=1 的部分,dp1,j 显然为 1 的单调递增的一次函数。

而进一步的,dpi,j 为以 j 为自变量的,斜率为 i 的一堆一次函数拼起来的东西。

考虑这些一次函数是怎么从最初的一个变为后来的多个的。

显然是因为一次取模造成的,而一次取模一定是将一个一次函数分为两个一次函数,并且一此取模只能将最多一个一次函数变为两个。

而我们观察到,对于一次有效的取模,amodb,必然会使 a 减半,也就是说,这个一次函数的个数只有 logV 个。

由于我们的答案本质上只关注的是这个一次函数的最高点。而碰巧的是,如果我们知道了一个一次函数的最高点,我们可以通过这个最高点求出他裂成两个一次函数的两个最高点和分别的答案。

于是我们可以优化一下这个 DP 状态,即我们定义 dpi,j,而这个 j 我们所有有用的只有一次函数的顶点。

其实剩下的细节就可以直接看代码了,重点是去理解这个有用的状态只有 O(nlogV) 级别。

当然这个 j 有可能比较大,打个 map 方便转移。

可以上代码了:

#include <bits/stdc++.h>
using namespace std;
#define maxn 200005
#define ll long long
int n;
ll a[maxn];
map<ll, ll> dp;
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]);
    dp[a[1] - 1] = 0;
    for (int i = 2; i <= n; ++i)
    {
        for (auto j = dp.lower_bound(a[i]); j != dp.end(); dp.erase(j++))
        {
            pair<ll, ll> k = *j;
            dp[k.first % a[i]] = max(dp[k.first % a[i]], k.second + (k.first - k.first % a[i]) * (i - 1));
            dp[a[i] - 1] = max(dp[a[i] - 1], k.second + (i - 1) * ((k.first + 1) / a[i] * a[i] - a[i]));
        }
    }
    ll ans = 0;
    for (auto i = dp.begin(); i != dp.end(); ++i)
    {
        pair<ll, ll> j = *i;
        ans = max(ans, j.first * n + j.second);
    }
    cout << ans << endl;
}

History / 历史年份

首先对于字符串中判断 [l1,r1],[l2,r2] 这两个区间所代表的数值的大小,可以简单打一个哈希,注意一下前导 0 即可,复杂度应该是 O(logn)

考虑到你要让最后一个数越小,而前面的数越大,我们可以考虑先确定最小的最后一个数,然后去看最大的最前面的数,分开算即可。

首先定义一个 dpi 表示 [dpi,i] 为选择的上一个数。

显然对于 [[1,dpi1],i] 都可以成为合法的方案。

转移就是枚举一个 j,看 [dpj,j][j+1,i]dpi=max(dpi,j+1),显然 dpi 越大对后面的转移最有利,也就与我们 [1,dpi1] 都合法的意愿刚好吻合。(如果不理解也可以写一个暴力的 dpi,j,即最后选择的数为 [j,i],看是否存在这样的方案,观察到对于每一个 i,对于最大 jdpi,j1,显然有 dpi,[1,j]=1

所以此时最后一个数最小就是 [dpn,n]

我们再考虑一个 fi 表示从尾到头选的最后一个数为 [i,fi],由于我们需要让前面的数越大,于是我们也希望 fi 越大,转移显然与 dp 相同,而正确性和理解方式也和 dp 相同。

当然注意一下 f 的初始值,最开始只有 f[dp[n]]=n,为了满足最后一个数最小的限制,当然注意一下前导 0 的特殊处理。

这样一个 O(n2logn) 的暴力就有了。

大致是这样:

#include <bits/stdc++.h>
using namespace std;
#define maxn 2005
#define ull unsigned long long
char a[maxn];
ull Has[maxn], p[maxn];
int n;
ull get(int l, int r) {return Has[r] - Has[l - 1] * p[r - l + 1];}
int pos[maxn];
bool cmp(int l1, int r1, int l2, int r2)
{
    l1 = min(pos[l1], r1), l2 = min(pos[l2], r2);
    if(r1 - l1 + 1 != r2 - l2 + 1) return r1 - l1 + 1 < r2 - l2 + 1;
    int l = 1, r = (r2 - l2 + 1), mid, ans = 0;
    while(l <= r)
    {
        mid = (l + r) >> 1;
        if(get(l1, l1 + mid - 1) == get(l2, l2 + mid - 1)) l = mid + 1, ans = mid;
        else r = mid - 1;
    }
    if(ans == r2 - l2 + 1) return false;
    return a[l1 + ans] < a[l2 + ans];
}
int dp[maxn], f[maxn];
int main()
{
    p[0] = 1;
    for (int i = 1; i <= 2000; ++i) p[i] = p[i - 1] * 13331;
    while(~scanf("%s", a + 1))
    {
        n = strlen(a + 1);
        for (int i = 1; i <= n; ++i) Has[i] = Has[i - 1] * 13331 + a[i];
        memset(dp, 0xf3, sizeof(dp));
        memset(f, 0xf3, sizeof(f));
        pos[n + 1] = n + 1;
        for (int i = n; i >= 1; --i)
        {
            if(a[i] == '0') pos[i] = pos[i + 1];
            else pos[i] = i;
        }
        dp[1] = 1;
        for (int i = 2; i <= n; ++i)
        {
            dp[i] = 1;
            for (int j = i - 1; j >= 1; --j)
            {
                if(cmp(dp[j], j, j + 1, i))
                {
                    dp[i] = j + 1;
                    break;
                }
            }
        }
        int now = dp[n];
        f[dp[n]] = n;
        for (int i = dp[n] - 1; i; --i)
        {
            if(a[i] != '0') break;
            f[i] = n;
            now = i;
        }
        for (int i = now - 1; i >= 1; --i)
        {
            for (int j = n; j >= i + 1; --j)
            {
                if(f[j] == 0xf3f3f3f3) continue;
                if(cmp(i, j - 1, j, f[j]))
                {
                    f[i] = j - 1;
                    break;
                }
            }
        }
        // for (int i = 1; i <= n; ++i) cout << i << " " << dp[i] << " " << f[i] << endl;
        now = 1;
        while(1)
        {
            for (int i = now; i <= f[now]; ++i) putchar(a[i]);
            now = f[now] + 1;
            if(now > n) break;
            putchar(',');
        }
        puts("");
    }
}

考虑一下如何优化,先只看 dp。我们发现只有满足 [dpj,j][j+1,i] 才能转移,而这里面有 3 个是跟 j 有关的。一个很神奇的事实是,在我们知道 j 的情况下,符合条件的 i 是单调的。

换句话说,每当我们求出一个 dpj 之后,我们可以直接通过二分找到它能够更新到的所有 i,而且一定是一个区间。

你可以考虑写一个线段树,当然双指针也是可以的。

f 同理。

于是这个题就有了,但是我还没有写,代码的话你可以参见 PYT 的。

#include<bits/stdc++.h>
using namespace std;
#define N 105050
int f[N],g[N];
char s[N];
#define ull unsigned long long 
const ull p=13331;
ull pw[N],h[N];
ull get(int l,int r){
    return h[r]-h[l-1]*pw[r-l+1];
}
int pos0[N];
bool com(int l1,int r1,int l2,int r2){
    l1=min(r1+1,pos0[l1]),l2=min(r2+1,pos0[l2]);
    int len1=r1-l1+1,len2=r2-l2+1;
    if(len1==len2){
        if(get(l1,r1)==get(l2,r2))return 0;
        int L=1,R=len1;
        while(L<R){
            int mid=L+R>>1;
            if(get(l1,l1+mid-1)==get(l2,l2+mid-1))L=mid+1;
            else R=mid;
        }
        return s[l1+L-1]<s[l2+L-1];
    }
    return len1<len2;
}
vector<int>gxf[N],gxg[N];
int q[N],ha,ti,delt[N];
signed main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    pw[0]=1;
    for(int i=1;i<=2000;++i)pw[i]=pw[i-1]*p;
    while(cin>>s+1){
        int n=strlen(s+1);
        for(int i=1;i<=n;++i){
            h[i]=h[i-1]*p+s[i];gxf[i].clear();gxg[i].clear();delt[i]=0;
        }pos0[n+1]=n+1;
        int cnt=0;
        for(int i=n;i;--i){
            if(s[i]=='0')pos0[i]=pos0[i+1];
            else pos0[i]=i;
            cnt+=(s[i]=='0');
        }
        if(n-cnt<=1){
            cout<<s+1<<"\n";continue;
        }
        for(int i=1;i<=n;++i)f[i]=g[i]=0;
        int mx=0;
        for(int i=1;i<=n;++i){
            for(auto x:gxf[i])mx=max(mx,x);
            f[i]=mx;++f[i];
            if(i!=n&&com(f[i],i,i+1,n)){
                int l=i+1,r=n;
                while(l<r){
                    int mid=l+r>>1;
                    if(com(f[i],i,i+1,mid))r=mid;
                    else l=mid+1;
                }
                gxf[l].push_back(i);
            }
        }
        s[0]='1';
        g[f[n]]=n;
        set<int>sta;
        int o=f[n]-1;
        for(;o&&s[o]=='0';)g[o]=n,--o;ha=1,ti=0;
        for(int i=o+1;i<=f[n];++i){
            if(i>1&&com(i-1,i-1,i,g[i])){
                int l=1,r=i-1;
                while(l<r){
                    int mid=l+r>>1;
                    if(com(mid,i-1,i,g[i]))r=mid;
                    else l=mid+1;
                }
                delt[i]=l-1;
            }
            else delt[i]=0x3f3f3f3f;
        }
        int up=f[n];
        for(int i=o;i;--i){
            while(delt[up]>=i)--up;
            g[i]=up;--g[i];
            if(i>1&&com(i-1,i-1,i,g[i])){
                int l=1,r=i-1;
                while(l<r){
                    int mid=l+r>>1;
                    if(com(mid,i-1,i,g[i]))r=mid;
                    else l=mid+1;
                }
                delt[i]=l-1;
            }
            else delt[i]=0x3f3f3f3f;
        }
        int p=1;
        while(p<=n){
            for(int i=p;i<=g[p];++i)cout<<s[i];if(g[p]!=n)cout<<",";p=g[p]+1;
        }cout<<"\n";
    }
}

后记

也是艰难写完总结,写完才发现怎么讨论区有一个标题跟我一模一样的啊(((

When all else is lost the future still remains.

posted @   Saltyfish6  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
Document
点击右上角即可分享
微信分享提示