传送门:CF
[前题提要]:自定义偏序来优化dp的递推,感觉这个trick很好,故记录一下
考虑对于同一张专辑,显然有贡献的序列是一个递增序列,所以我们可以直接对此进行删减.
接下来我们就获得了一些专辑,并且每张专辑的价值都是递增的.我们现在需要解决的问题是如何排列我们的专辑使得最后的贡献最大.
其实此时是不难想到 d p dp dp的,但是该如何 d p dp dp呢.
不难发现当我们枚举到第 i i i个物品的时候,我们想将其加入到我们之前的序列中,我们需要知道前缀的最大值,这样我们才能知道此时的 i i i增加的贡献是多少,所以我们考虑用一个 d p [ i ] [ j ] dp[i][j] dp[i][j]来记录加入了前 i i i个专辑,前缀最大值为 j j j时的最大贡献,但是此时我们又会发现存在这样一个问题,我们此时将 i i i插入到 j j j的后面,此时我们 i i i的贡献不难算出了,但是此时的 i i i会影响到前缀最大值,也就是会导致后序的贡献改变了.
也就是对于任意一个专辑,我们将其放在一个位置,它的贡献既会影响到前驱又会影响到后继.此时就很难进行维护了.此时思维似乎陷入了死胡同.
此时想一下是什么导致我们无法继续进行下去,是因为当前插入的那个物品影响到了后序的贡献,那么有没有一种做法,我们不会影响到后面呢.诶,你会发现只要当前插入的那个物品的最大值是之前i个最大的,那么此时无论我们的物品插入到哪里,后续的贡献都是0,也就是说,此时后续的贡献就是固定为0,也就不难维护了.顺着这个思路,我们会想到按最大值进行排序,这样就可以保证每次插入的物品都是前缀最大值了.(此处不得不吐槽一下,几乎所有的题解对此处的排序的解释都是贪心性排序,反正博主觉得根本不是这么一回事,此处的排序纯纯的只是为了我们的dp的递推方便而已,至于为什么它们这么一致,那就智者见智了)
此时插入一条简单的证明:我们对加入物品的顺序进行排序,并不会影响我们最终的最优性策略.感性的想一下就是我们运用的方法是动态规划,也就是我们将所有的可能性都是保留的,在最后一刻才将所有的策略取一个最优解.理性的想一下:考虑对一串序列 a 1 , a 2 , a 3 , . . , a n a_1,a_2,a_3,..,a_n a1,a2,a3,..,an,我们此时分别需要插入 a n + 1 , a n + 2 a_{n+1},a_{n+2} an+1,an+2这两个数字,我们考虑先加入 a n + 1 a_{n+1} an+1再加入 a n + 2 a_{n+2} an+2,假设上述两个数字分别插在 p o s 1 , p o s 2 pos1,pos2 pos1,pos2的位置,显然这两个位置是互不影响的(也就是说第一个位置并不会影响第二个位置的存放),所以当我们先加入 a n + 2 a_{n+2} an+2的时候,我们此时仍然可以将其放在 p o s 2 pos2 pos2的位置.并且对于动态规划来说,上述四种状态都会被存下来,所以正确性是对的.
所以考虑先对其进行排序,然后考虑用一个 d p [ i ] [ j ] dp[i][j] dp[i][j]来记录加入了前 i i i个专辑,前缀最大值为 j j j时的最大贡献即可.为了快速的查找最大值,这是个 R M Q RMQ RMQ问题,我们可以使用多种数据结构来进行动态维护.博主使用的是权值线段树.
需要注意的一点细节是并不能对于每一个测试样例都建一棵树,这样的复杂度会假.(别问为什么我知道,因为我实现的时候就是这么实现的,然后T了.此时让我想起了之前每次实现权值线段树的时候要么是离线,要么没有多组数据,所以一直没有遇到过这种情况).当然如果你的dp方程稍微改一下,改成前i个专辑,前缀最大值的下标是j专辑的最大贡献,这样就可以每次都建一颗树了.
下面是具体的代码部分:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls (rt<<1)
#define rs (rt<<1|1)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {ll x=0,w=1;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';return x*w;
}
inline void print(__int128 x){if(x<0) {putchar('-');x=-x;}if(x>9) print(x/10);putchar(x%10+'0');
}
#define maxn 1000000
const double eps=1e-8;
#define int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
struct Segment_tree{int l,r,mx;
}tree[maxn<<2];
void pushup(int rt) {tree[rt].mx=max(tree[ls].mx,tree[rs].mx);
}
void build(int l,int r,int rt) {tree[rt].l=l;tree[rt].r=r;tree[rt].mx=-int_INF;if(l==r) {return ;}int mid=(l+r)>>1;build(lson);build(rson);pushup(rt);
}
void update(int pos,int val,int rt) {if(tree[rt].l==pos&&tree[rt].r==pos) {tree[rt].mx=max(tree[rt].mx,val);return ;}int mid=(tree[rt].l+tree[rt].r)>>1;if(pos<=mid) update(pos,val,ls);else update(pos,val,rs);pushup(rt);
}
void reset(int pos,int val,int rt) {if(tree[rt].l==pos&&tree[rt].r==pos) {tree[rt].mx=val;return ;}int mid=(tree[rt].l+tree[rt].r)>>1;if(pos<=mid) reset(pos,val,ls);else reset(pos,val,rs);pushup(rt);
}
int query(int l,int r,int rt) {if(tree[rt].l==l&&tree[rt].r==r) {return tree[rt].mx;}int mid=(tree[rt].l+tree[rt].r)>>1;if(r<=mid) return query(l,r,ls);else if(l>mid) return query(l,r,rs);else return max(query(l,mid,ls),query(mid+1,r,rs));
}
int k[maxn];vector<int>a[maxn];int dp[maxn];
bool cmp(vector<int>&A,vector<int>&B) {return A.back()<B.back();
}
int main() {int T=read();build(1,2e5,1);while(T--) {int n=read();for(int i=1;i<=n;i++) {k[i]=read();for(int j=1;j<=k[i];j++) {int num=read();if((int)a[i].size()!=0&&num<=a[i].back()) {continue;}else {a[i].push_back(num);}}k[i]=a[i].size();}
// build(1,2e5,1);这里复杂度假了,Tvlogvsort(a+1,a+n+1,cmp);for(int i=1;i<=n;i++) {for(int j=0;j<a[i].size();j++) {if(a[i][j]==1) {dp[a[i].back()]=max(dp[a[i].back()],(int)a[i].size()-j);}else {int num=query(1,a[i][j]-1,1);if(num==-int_INF) num=0;dp[a[i].back()]=max(dp[a[i].back()],num+(int)a[i].size()-j);}}update(a[i].back(),dp[a[i].back()],1);}int ans=-int_INF;for(int i=1;i<=n;i++) {ans=max(ans,dp[a[i].back()]);}printf("%d\n",ans);//clearfor(int i=1;i<=n;i++) {reset(a[i].back(),-int_INF,1);dp[a[i].back()]=0;a[i].clear();}}return 0;
}