新高一暑假集训随记2
8.13
还是 DP。
「JOI Open 2016」摩天大楼
我们发现最后的序列按大小肯定会形成一个类似波浪的形状,而对于一个长度为 \(k\) 的段满足:\(f_1\le f_2\le f_3\le \cdots\le f_p\ge \cdots \ge f_{k-1}\ge f_k\),其最后贡献的权值为 \(f_p-f_1+f_p-f_k\),即两倍最大值减左端点和右端点。
考虑对这种连续段进行 DP,将 \(A\) 从大到小排序,令 \(dp_{i,j,k,0/1,0/1}\)。表示前 \(i\) 个数分成了 \(j\) 段现在的总贡献为 \(k\),是否确定了序列开头/结尾的方案数。
新建段时确定段之间的顺序,转移十分的经典:
- \(A_i\) 单开新的一段:\(dp_{i,j+1,k+2A_i,x,y}\overset{+}\leftarrow dp_{i-1,j,k,x,y}\times(j+1-x-y)\)。
- \(A_i\)接在一个段的开头或结尾:\(dp_{i,j,k,x,y}\overset{+}\leftarrow dp_{i-1,j,k,x,y}\times(2j-x-y)\)。
- \(A_i\) 连接两个段:\(dp_{i,j-1,k-2A_i,x,y}\overset{+}\leftarrow dp_{i,j,k,x,y}\times(j-1)\)。
- \(A_i\) 接在第一个段前固定为序列开头:\(dp_{i,j,k-A_i,1,y}\overset{+}\leftarrow dp_{i-1,j,k,0,y}\)。
- \(A_i\) 接在最后一个段后固定为序列结尾:\(dp_{i,j,k-A_i,x,1}\overset{+}\leftarrow dp_{i-1,j,k,x,0}\)。
- \(A_i\) 单开一个段并作为序列开头:\(dp_{i,j+1,k+A_i,1,y}\overset{+}\leftarrow dp_{i-1,j,k,0,y}\)。
- \(A_i\) 单开一个段并作为序列结尾:\(dp_{i,j+1,k+A_i,x,1}\overset{+}\leftarrow dp_{i-1,j,k,x,0}\)。
这个时候我们发现 \(k\) 这一维由于有减法必须记录所有 \(O(nL)\) 个状态,开不下怎么办?
考虑一个差分的思想,将贡献的 \(A_i-A_j\) 拆成 \((A_i-A_{i+1})+(A_{i+1}-A_{i+2})+\cdots+(A_{j-1}-A_{j})\)。由于排序后 \(A_i \ge A_{i+1}\),所以这里面每一项都是正的。
同时我们发现,当我们加入 \(A_i\) 后,若分成了 \(j\) 段,确定开头结尾的状态分别是 \(x,y\) 则总权值会增加 \((j-x-y)(A_i-A_{i+1})\),因为有这么多段的开头或结尾之后需要再添加 \(A_i\) 之后的数。
DP 时注意边界条件,如 \(j>0\) 才可进行转移 \(2,3,5\),\(j>1\) 才可进行转移 \(3\)。
最后答案是 \(\sum\limits_{i=0}^Ldp_{n,1,i,1,1}\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
#define int ll
const int N=105,C=1005,mod=1e9+7;
ll n,L,a[N],dp[2][N][C][2][2],now=1,pre=0,ans;
void add(ll& x,ll y){x=(x+y)%mod;}
void up(int a,ll b,int c,int d,int e,int f,int g,int h,ll p){assert(b>=0),add(dp[now][a][min(L+1,b)][c][d],p*dp[pre][e][f][g][h]%mod);}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>L;
for(int i=1;i<=n;i++) cin>>a[i];
if(n==1) return cout<<1<<"\n",0;
sort(a+1,a+n+1),reverse(a+1,a+n+1);
dp[pre][0][0][0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=i;j++){
for(int k=0;k<=L;k++){
for(int x=0;x<2;x++){
for(int y=0;y<2;y++) dp[now][j][k][x][y]=0;
}
}
}
for(int j=0;j<i;j++){
for(int k=0;k<=L;k++){
for(int x=0;x<2;x++){
for(int y=0;y<2;y++){
up(j+1,k+(2*(j+1)-x-y)*(a[i]-a[i+1]),x,y,j,k,x,y,(j+1-x-y));
if(!x) up(j+1,k+(2*(j+1)-1-y)*(a[i]-a[i+1]),1,y,j,k,x,y,1);
if(!y) up(j+1,k+(2*(j+1)-x-1)*(a[i]-a[i+1]),x,1,j,k,x,y,1);
if(j){
up(j,k+(2*j-x-y)*(a[i]-a[i+1]),x,y,j,k,x,y,(j*2-x-y));
if(j>1) up(j-1,k+(2*(j-1)-x-y)*(a[i]-a[i+1]),x,y,j,k,x,y,(j-1));
if(!x) up(j,k+(2*j-1-y)*(a[i]-a[i+1]),1,y,j,k,x,y,1);
if(!y) up(j,k+(2*j-x-1)*(a[i]-a[i+1]),x,1,j,k,x,y,1);
}
}
}
}
}
swap(now,pre);
}
for(int i=0;i<=L;i++) add(ans,dp[pre][1][i][1][1]);
cout<<ans<<"\n";
}
[ABC266Ex] Snuke Panic (2D)
考虑只能朝 \(y\) 轴正方向移动,则到达第 \(j\) 个点后,想要获得第 \(i\) 个点的分数,必然满足以下条件:
- \(y_j\le y_i\)。
- \(t_j\le t_i\)。
- \(y_i-y_j+|x_i-x_j|\le t_i-t_j\)。
然后我们发现第三个条件其实蕴含了第二个条件,所以可以去掉第二个条件,即是:
- \(y_j\le y_i\)。
-
\[\left\{\begin{matrix}t_i-y_i-x_i\ge t_j-y_j-x_j,(x_i\ge x_j) \\t_i-y_i+x_i\ge t_j-y_j+x_j,(x_i< x_j)\end{matrix}\right. \]
这是一个三维偏序,最外层按 \(y\) 排序,CDQ 解决 \(x\) 的偏序,树状数组统计第三层,更新 DP 数组即可。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e5+5;
struct node{ll t,x,y,v,sub,add,num;}a[N];
bool cmp1(node a,node b){return a.y==b.y?a.t<b.t:a.y<b.y;}
bool cmp2(node a,node b){return a.x==b.x?a.t<b.t:a.x<b.x;}
struct BIT{
ll t[N<<1];
inline int lb(int x){return x&(-x);}
inline void change(int p,ll v){while(p<(N<<1)){t[p]=max(t[p],v),p+=lb(p);}}
inline void clear(int p){while(p<(N<<1)){t[p]=-0x3f3f3f3f3f3f3f3f,p+=lb(p);}}
inline ll query(int p,ll ret=-0x3f3f3f3f3f3f3f3f){while(p){ret=max(ret,t[p]),p-=lb(p);} return ret;}
}T;
ll n,b[N<<1],dp[N],cnt,ans;
void cdq(int l,int r){
// cerr<<l<<" "<<r<<"\n";
if(l==r) return;
int mid=(l+r)>>1,j=l;
cdq(l,mid);
sort(a+l,a+mid+1,cmp2),sort(a+mid+1,a+r+1,cmp2);
for(int i=mid+1;i<=r;i++){
while(j<=mid&&a[j].x<=a[i].x) T.change(a[j].sub,dp[a[j].num]),j++;
dp[a[i].num]=max(dp[a[i].num],T.query(a[i].sub)+a[i].v);
}
for(int i=l;i<=j;i++) T.clear(a[i].sub);
reverse(a+l,a+mid+1),reverse(a+mid+1,a+r+1);j=l;
for(int i=mid+1;i<=r;i++){
while(j<=mid&&a[j].x>a[i].x) T.change(a[j].add,dp[a[j].num]),j++;
dp[a[i].num]=max(dp[a[i].num],T.query(a[i].add)+a[i].v);
}
for(int i=l;i<=j;i++) T.clear(a[i].add);
sort(a+mid+1,a+r+1,cmp1),cdq(mid+1,r);
sort(a+l,a+r+1,cmp2);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n;memset(T.t,-0x3f,sizeof(T.t));
for(int i=1;i<=n;i++) cin>>a[i].t>>a[i].x>>a[i].y>>a[i].v,a[i].num=i,b[++cnt]=a[i].t-a[i].y-a[i].x,b[++cnt]=a[i].t-a[i].y+a[i].x;
n++,a[n]={0,0,0,0,0,0,n};b[++cnt]=0;
sort(b+1,b+cnt+1);cnt=unique(b+1,b+cnt+1)-b-1;
for(int i=1;i<=n;i++) a[i].sub=lower_bound(b+1,b+cnt+1,a[i].t-a[i].y-a[i].x)-b,a[i].add=lower_bound(b+1,b+cnt+1,a[i].t-a[i].y+a[i].x)-b;
sort(a+1,a+n+1,cmp1);
memset(dp,-0x3f,sizeof(dp));dp[n]=0;
cdq(1,n);
for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
cout<<ans<<"\n";
}
Optimal Binary Search Tree
最简单的一道,深度可以看作祖先个数,所以在自己和除根以外祖先统计即可。也就是除了根以外的结点子树和之和,对应过来就是区间和。
有一个简单的区间 DP,四边形不等式即可优化到 \(O(n^2)\)
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=2005;
ll n,a[N],S[N],dp[N][N],p[N][N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
while(cin>>n){
for(int i=1;i<=n;i++) cin>>a[i],S[i]=S[i-1]+a[i];
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++) dp[i][j]=0x3f3f3f3f3f3f3f3f,p[i][j]=0;
}
for(int i=1;i<=n;i++) dp[i][i]=dp[i][i-1]=0,p[i][i]=i;
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
for(int k=p[l][r-1];k<=p[l+1][r];k++){
if(dp[l][k-1]+dp[k+1][r]+S[r]-S[l-1]-a[k]<dp[l][r]) dp[l][r]=dp[l][k-1]+dp[k+1][r]+S[r]-S[l-1]-a[k],p[l][r]=k;
}
}
}
cout<<dp[1][n]<<"\n";
}
}
[ARC150F] Constant Sum Subsequence
对于一个序列 \(B\) 将其放入 \(A\) 使得最后一个数的位置编号最小可以贪心解决。
设 \(dp_i\) 表示以 \(i\) 结尾的能通过贪心放至第 \(i\) 个位置的序列的 \(S\) 最小值。
显然有 \(dp_i=\operatorname{min}_{lst_i\le j<i}dp_j+a_i\),其中 \(lst_i\) 是 \(a_i\) 上一次出现的位置,可以直接用线段树优化。
考虑 \(dp_i\) 有一个很好的性质:当 \(i\ge 2\) 时,\(dp_{i+n}-dp_i=dp_{i+2n}-dp_{i+n}\),感性理解是因为由于序列是循环的,而决策点不会变化,所以对于 \(1\sim 3n\) 跑一遍 DP 即可。
最后记录 \(dp_i=S\) 的最大的 \(i\) 即可,感性理解如果有 \(j>i\) 使得 \(dp_j<S\),且以第 \(j\) 个数结尾可以使得和为 \(S\),则必然存在一个 \(k>j\) 使得 \(dp_k=S\),因为可以通过调整得来。
当然,正解是 CDQ 分治优化 DP,更加有道理。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=4.5e6+5;
ll n,S,ans,a[N],dp[N],lst[N];
struct segment_tree_node{
ll mi;
void give_start_val(ll x){mi=x;}
friend segment_tree_node merge(segment_tree_node x,segment_tree_node y){return {min(x.mi,y.mi)};}
};
const segment_tree_node NOTHING={0x3f3f3f3f3f3f3f3f};
template<typename T,typename ST,int N> struct segment_tree{
T t[N<<2],Nothing;
segment_tree(T No){Nothing=No;}
inline int ls(int x){return x<<1;}
inline int rs(int x){return x<<1|1;}
#define mid ((l+r)>>1)
inline void push_up(int x){t[x]=merge(t[ls(x)],t[rs(x)]);}
void build(int p,int l,int r,ST* val){
if(l==r) return t[p].give_start_val(*(val+l)),void();
else{
build(ls(p),l,mid,val);
build(rs(p),mid+1,r,val);
push_up(p);
}
}
void change(int p,int l,int r,int pos,ST val){
if(l==r) return t[p].give_start_val(val),void();
else{
if(pos<=mid) change(ls(p),l,mid,pos,val);
else change(rs(p),mid+1,r,pos,val);
push_up(p);
}
}
T query(int p,int l,int r,int re_l,int re_r){
if(re_l<=l&&r<=re_r) return t[p];
else if(!(r<re_l||l>re_r)) return merge(query(ls(p),l,mid,re_l,re_r),query(rs(p),mid+1,r,re_l,re_r));
else return Nothing;
}
};
segment_tree<segment_tree_node,ll,N>T(NOTHING);
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>S;
for(int i=1;i<=n;i++) cin>>a[i],a[i+n+n]=a[i+n]=a[i];
T.change(1,0,n*3,0,0);
for(ll i=1;i<=n*3;i++){
dp[i]=a[i]+T.query(1,0,n*3,lst[a[i]],i-1).mi;
T.change(1,0,n*3,i,dp[i]),lst[a[i]]=i;
if(dp[i]==S) ans=max(ans,i);
}
for(int i=1;i<=n;i++){
ll tmp=dp[n+n+i]-dp[n+i];
if(S>dp[n+n+i]&&(S-dp[n+n+i])%tmp==0) ans=max(ans,(S-dp[n+n+i])/tmp*n+n+n+i);
}
cout<<ans<<"\n";
}
8.14
怎么题越来越多了?
怎么由难到易排序?
怎么全都正序开题!!!
[ARC125F] Tree Degree Subset Sum
树的条件只是用来限制 \(\sum d_i=2n-2\) 的,然后就没用了。
考虑到每个点度数至少为 \(1\),凭直觉不妨给每一个结点度数减 \(1\),实则创造更多的 \(0\) 也便于观察更多性质。
发现当 \(\sum d_i=n-2\) 时,\(d_i\) 的种类只有 \(O(\sqrt n)\) 种,这启发我们使用 \(O(n)\) 的复杂度对单个种类进行查询,很明显应该是单调队列优化多重背包。
但是选择的点数不同也算作本质不同的方案,直接 DP 时间复杂度来到了 \(O(n^2\sqrt{n})\),寄掉了。
考虑到选择度数为 \(0\) 的结点起作用只有将选择点数增加 \(1\),考虑到有 \(k\) 个这样的点,则对于一个未选择度数为 \(0\) 的点的合法方案 \((x,y)\),则 \(\forall p\in[x,x+k],(p,y)\) 是一个合法的方案,这告诉我们,当 \(y\) 确定时,使 \((x,y)\) 合法的 \(x\) 组成的连续段必然不会很多。
可以断言,对于同一个 \(y\),合法的 \(x\) 只组成一个连续段。
因为对于一个合法的方案 \((x,y)\),必然有 \(y-x\in[-k,n-3]\) 且同时 \((n-x,n-2-y)\) 必定合法,则有 \(x-y-2\in[-k,n-3]\),所以有 \(x\in[3-n,y+k]\cap[y-k+2,y+n-1]=[y-k+2,y+k]\),由于 \(x_{max}\) 一定选择的所有度数为 \(0\) 的结点,\(x_{min}\) 则一个都不会选择,那么对于 \(x\in[x_{min},x_{min}+k]\cup[x_{max}-k,x_{max}]\),\(x\) 必定合法,又因为 \(x_{max}-x_{min}=2k-2\),所以上面两个区间相交,得证。
所以可以只记录不选择度数为 \(0\) 的结点时(不选度数为 \(0\) 结点是因为不能进行 DP,最后加回来即可),\(y=i\) 时 \(x\) 的最大最小值,时间复杂度被优化至 \(O(n\sqrt n)\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const ll N=2e5+5,INF=0x3f3f3f3f3f3f3f3f;
ll n,d[N],zr,a[N],dpma[N],dpmi[N],tmp[N];
ll ans;
ll q[N],head,tail;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n;
for(int i=1,u,v;i<n;i++) cin>>u>>v,d[u]++,d[v]++;
for(int i=1;i<=n;i++) d[i]--,a[d[i]]++,dpma[i]=-INF,dpmi[i]=INF;dpma[0]=dpmi[0]=0;
for(int i=1;i<=n;i++){
if(a[i]){
for(int j=0;j<i;j++){
for(int k=j;k<=n;k+=i) tmp[k]=dpma[k];
head=1,tail=0;q[++tail]=0;
for(int k=1;j+k*i<=n;k++){
while(head<=tail&&q[head]+a[i]<k) head++;
while(head<=tail&&dpma[j+q[head]*i]-q[head]<=dpma[j+k*i]-k) tail--;
q[++tail]=k;tmp[j+k*i]=dpma[j+q[head]*i]-q[head]+k;
}
for(int k=j;k<=n;k+=i) dpma[k]=tmp[k];
for(int k=j;k<=n;k+=i) tmp[k]=dpmi[k];
head=1,tail=0;q[++tail]=0;
for(int k=1;j+k*i<=n;k++){
while(head<=tail&&q[head]+a[i]<k) head++;
while(head<=tail&&dpmi[j+q[head]*i]-q[head]>=dpmi[j+k*i]-k) tail--;
q[++tail]=k;tmp[j+k*i]=dpmi[j+q[head]*i]-q[head]+k;
}
for(int k=j;k<=n;k+=i) dpmi[k]=tmp[k];
}
}
}
for(int i=0;i<=n;i++){
if(dpma[i]>=dpmi[i]) ans+=dpma[i]-dpmi[i]+1+a[0];
}
cout<<ans<<"\n";
}
//dp[j+ki]=max(dp[j+pi]-p)+k
CF1152F2 Neko Rules the Catniverse (Large Version)
-
任意两个元素其权值不同。
-
对于任意 \(i\) 满足 \(1\le i\le k\) 有 \(1\le a_i\le n\)。
-
对于任意 \(i\) 满足 \(2\le i\le k\) 有 \(a_i\le a_{i-1}+m\)。
答案对 \(10^9+7\) 取模。
数据范围:
\(1\le n\le 10^9\),\(1\le k\le \min(n,12)\),\(1\le m\le 4\)。
先考虑 \(1\le n\le 10^5\) 即弱化版应该怎么做,考虑到约束只与值有关,考虑给值设计状态,钦定枚举 \(i\) 从小到大插入序列中满足条件 \(1\),发现 \(m\) 很小,那么可以对目前到 \([i-m,i-1]\) 的使用状态进行状压。
则 \(dp_{i,j,S}\) 表示当前枚举到了 \(i\),序列长度为 \(j\),\([i-m,i-1]\) 的使用状态为 \(S\) 是的方案数。
那么有转移:
那么就可以在 \(O(nk2^m)\) 的时间内解决弱化版。
我们发现 \(k2^m\) 不大,直接上矩阵快速幂即可,\(O(k^38^m\log n)\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int K=13,N=4,M=K*17+1,mod=1e9+7;
ll n,k,m,p;
struct martix{
ll m[M][M];
martix(int i=0){
memset(m,0,sizeof(m));
if(i==1) for(int j=0;j<M;j++) m[j][j]=1;
}
friend bool operator==(martix a,martix b){
for(int i=0;i<M;i++)
for(int j=0;j<M;j++)
if(a.m[i][j]!=b.m[i][j]) return 0;
return 1;
}
friend martix operator+(martix a,martix b){
martix c(0);
for(int i=0;i<M;i++)
for(int j=0;j<M;j++)
c.m[i][j]=(a.m[i][j]+b.m[i][j])%mod;
return c;
}
friend martix operator*(martix a,martix b){
martix c(0);
for(int i=0;i<M;i++)
for(int k=0;k<M;k++)
for(int j=0;j<M;j++)
c.m[i][j]=(c.m[i][j]+a.m[i][k]*b.m[k][j])%mod;
return c;
}
void operator*=(martix b){*this=*this*b;}
ll* operator[](size_t x){return m[x];}
void print(){
for(int i=0;i<M;i++){
for(int j=0;j<M;j++) cout<<m[i][j]<<" ";
cout<<'\n';
}
cout<<'\n';
}
}st,mtx;
template<typename T>
void q_pow(T& st,T mtx,ll x){
// count st*(mtx^x)
while(x){
if(x&1) st*=mtx;
mtx*=mtx;
x>>=1;
}
}
ll get(ll x,ll y){return x*p+y;}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>k>>m;p=1<<m;
st[0][get(0,0)]=1;
for(int j=0;j<k;j++){
for(int S=0;S<p;S++){
mtx[get(j,S)][get(j+1,(S<<1|1)&(p-1))]=__builtin_popcount(S)+1;
mtx[get(j,S)][get(j,(S<<1)&(p-1))]=1;
}
}
for(int S=0;S<p;S++) mtx[get(k,S)][(k+1)*p]=1;
mtx[(k+1)*p][(k+1)*p]=1;
q_pow(st,mtx,n+1);
cout<<st[0][(k+1)*p]<<"\n";
}
CF1242C Sum Balance
判断无解是平凡的。
考虑只有一次操作,则对于每一个盒子的每一个物品,拿出其只需要一个固定的值。
让每个物品向其需要的物品连一条边,则整张图形成一个内向基环树森林,而一个环代表我们可以拿出其上所有的点进行操作使得这些盒子均满足条件。
由于 \(k\) 比较小,所以可以考虑状压,找出所有的环后,记录每个环覆盖的点,状压 DP 并输出方案即可。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const ll K=15,N=5005;
#define int ll
ll k,S,add[K+5],n[K+5],a[K+5][N],cnt,dp[1<<K],pre[1<<K],in[1<<K];
ll ansv[K+5],anst[K+5];
bool vis[1<<K];
map<ll,PII>ma;
vector<pair<ll,PII>>cyc[1<<K];
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>k;
for(int i=1;i<=k;i++){
cin>>n[i];
for(int j=1;j<=n[i];j++) cin>>a[i][j],S+=a[i][j],add[i]+=a[i][j],ma[a[i][j]]={i,j};
}
if(S%k) return No();
else{
S/=k;
for(int i=1;i<=k;i++){
add[i]=S-add[i];
for(int j=1;j<=n[i];j++){
ll tmp=a[i][j],now=0;
vector<pair<ll,PII>>cyctmp;
while(1){
ll in=ma[tmp].fi;
tmp+=add[in];
if(!ma.count(tmp)) goto end;
PII pos=ma[tmp];
if(now&(1<<(pos.fi-1))) goto end;
now|=(1<<(pos.fi-1));
cyctmp.push_back({in,pos});
if(pos.fi==i){
if(pos.se!=j) goto end;
break;
}
}
// cerr<<now<<"\n";
vis[now]=1;cyc[now]=cyctmp;
end:;
}
}
dp[0]=1;
for(int S=1;S<(1<<k);S++){
for(int P=S;P;P=(P-1)&S){
if(vis[P]&&dp[S^P]) pre[S]=P,dp[S]=1;
}
}
if(!dp[(1<<k)-1]) return No();
else{
ll tmp=(1<<k)-1;
while(tmp){
for(auto it:cyc[pre[tmp]]) ansv[it.se.fi]=a[it.se.fi][it.se.se],anst[it.se.fi]=it.fi;
tmp^=pre[tmp];
}
Yes();
for(int i=1;i<=k;i++) cout<<ansv[i]<<" "<<anst[i]<<"\n";
}
}
}
「SMOI-R2」Monotonic Queue
诡异蓝题。
考虑到每一个区间 \(l_i\) 的作用只有防止 \(r_i\) 踢掉更多的数,所以我们可以利用 \(l_i\) 在踢到最优解时停下。
由于加入的数不会随区间变化而变化,即对于同一个前缀,在不考虑 \(l_i\) 的情况下最后的单调队列是固定的。
考虑设计 DP,\(dp_i\) 表示最后一个区间右端点为 \(i\) 时的答案,答案即为 \(\max_{i=1}^ndp_i\)。
由于队列形态固定,所以可以在枚举 \(i\) 的同时维护一个单调队列,在弹数的同时更新 DP。
设当前加入 \(i\) 会导致 \(j\) 被弹出,则有 \(dp_i\overset{max}\leftarrow dp_j+S_{j,i-1}\),其中 \(S_{j,i-1}=\sum\limits_{k=j}^{i-1} c_k\)。 因为当 \(j\) 被弹出时,\([j,i-1]\) 的所有数肯定已经弹出了,而这些 \(dp_j\) 并不会统计。
特别的,当加入 \(i\) 被 \(j\) 挡住时,即 \(a_j>a_i\),有 \(dp_i\overset{max}\leftarrow dp_j+S_{j+1,i-1}\)。因为 \(dp_j\) 并不会被弹出。
令 \(a_0=n+1\),开始时塞入队列,可避免掉对队列为空的判断。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=5e5+5;
ll n,c[N],a[N],q[N],head=1,tail,dp[N],ans=-0x3f3f3f3f3f3f3f3f;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n;
for(int i=1;i<=n;i++) cin>>c[i],c[i]+=c[i-1];
for(int i=1;i<=n;i++) cin>>a[i];a[0]=n+1;
q[++tail]=0;memset(dp,-0x3f,sizeof(dp));dp[0]=0;
for(int i=1;i<=n;i++){
while(a[q[tail]]<a[i]) dp[i]=max(dp[i],c[i-1]-c[q[tail]-1]+dp[q[tail]]),tail--;
dp[i]=max(dp[i],c[i-1]-c[q[tail]]+dp[q[tail]]);
q[++tail]=i;
ans=max(ans,dp[i]);
}
cout<<ans<<"\n";
}
[NOI2011] NOI 嘉年华
发现最终会将选择的区间分为两个时间不重叠的时间段集,可以考虑将时间离散化后按时间 DP。
先考虑第一问。
设 \(dp_{i,j}\) 表示所有 \(r_x\le i\) 的活动中有 \(j\) 个在这个场地举办,另一个场地举办活动个数的最大值。
贪心地想,被一个区间包含的活动只要选了一个就得选完,显然有 \(dp_{i,j}=\max_{k=1}^{i}\max\{dp_{k,j}+cnt_{k,i},dp_{k,j-cnt_{k,i}}\}\)。\(cnt_{k,i}\) 是包含于 \([k,i]\) 的区间数。
注意 \(dp_{k,j-cnt_{k,i}}\) 中的 \(j-cnt_{k,i}\) 需要对 \(0\) 取 \(\max\),因为当出现两个重复区间时,只选一个区间的情况不会被统计到,这就会导致答案单调性时指针移动失败被挡住了没有找到正确位置,至于真正不合法的情况一定不会成为最大值。
第一问的答案就是 \(\max_{i=0}^n\min\{dp_{m,i},i\}\),\(m\) 为离散化后的点数。
考虑第二问,强制选一个区间,则我们可以对前缀和后缀分别做一次以上 DP,然后强制选中间的所有区间,并和前缀后缀合并,记两次 DP 求出的数组分别为 \(pre,suf\)。
则有:
但是有可能会有一个活动包含 \([l_i,r_i]\),但其不会对 \(dp_{l,r}\) 产生贡献,所以强制选一个区间的答案应为 \(\max_{i=1}^{l_i}\max_{j=r_i}^m dp_{l,r}\)。
所以我们需要处理出所有 \(dp_{l,r}\),但这样是 \(O(n^4)\) 的,考虑优化。
我们发现,当固定 \(x\) 时,\(y\) 增大会导致 \(x+y+cnt_{l,r}\) 增大,\(pre_{l,x}+suf_{r,y}\) 减小,而想要取到最大值,两者的值必须相近,因为如果两者差距较大,则 \(\min\) 一定更小。
所以 \(y\) 的最优决策点随 \(x\) 的增大而减小,而 \(\min\{x+y+cnt_{l,r},pre_{l,x}+suf_{r,y}\}\) 近似为一个单峰函数,所以可以直接对 \(y\) 维护一个指针,将对 \(x,y\) 两维的枚举优化到 \(O(n)\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=205,INF=0x3f3f3f3f;
int n,l[N],r[N],S[N<<1][N<<1],pre[N<<1][N],suf[N<<1][N],dp[N<<1][N<<1],b[N<<1],cnt;
inline int calc(int l,int r,int x,int y){return min(S[l][r]+x+y,pre[l][x]+suf[r][y]);}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n;
for(int i=1,t;i<=n;i++) cin>>l[i]>>t,r[i]=l[i]+t,b[++cnt]=l[i],b[++cnt]=r[i];
sort(b+1,b+cnt+1),cnt=unique(b+1,b+cnt+1)-b-1;
for(int i=1;i<=n;i++) l[i]=lower_bound(b+1,b+cnt+1,l[i])-b,r[i]=lower_bound(b+1,b+cnt+1,r[i])-b;
for(int i=1;i<=cnt;i++){
for(int j=i;j<=cnt;j++){
for(int k=1;k<=n;k++){
if(i<=l[k]&&r[k]<=j) S[i][j]++;
}
}
}
for(int i=1;i<=cnt;++i){
for(int j=1;j<=n;++j) pre[i][j]=suf[i][j]=-INF;
}
for(int i=1;i<=cnt;i++){
for(int j=0;j<=n;j++){
for(int k=1;k<=i;k++){
pre[i][j]=max(pre[i][j],pre[k][j]+S[k][i]);
pre[i][j]=max(pre[i][j],pre[k][max(0,j-S[k][i])]);
}
}
}
for(int i=cnt;i>=1;i--){
for(int j=0;j<=n;j++){
for(int k=i;k<=cnt;k++){
suf[i][j]=max(suf[i][j],suf[k][j]+S[i][k]);
suf[i][j]=max(suf[i][j],suf[k][max(0,j-S[i][k])]);
}
}
}
for(int l=1;l<=cnt;l++){
for(int r=l+1;r<=cnt;r++){
for(int x=0,y=n;x<=n;x++){
while(y&&calc(l,r,x,y-1)>=calc(l,r,x,y)) y--;
dp[l][r]=max(dp[l][r],calc(l,r,x,y));
}
}
}
int ans=0;
for(int i=0;i<=n;i++) ans=max(ans,min(pre[cnt][i],i));
cout<<ans<<"\n";
for(int i=1;i<=n;i++){
ans=0;
for(int j=1;j<=l[i];j++){
for(int k=r[i];k<=cnt;k++) ans=max(ans,dp[j][k]);
}
cout<<ans<<"\n";
}
}
「SMOI-R2」Speaker
考虑选一个点会有几种情况。
- \(z\) 在 \(x\) 到 \(y\) 的路径上,此时权值为 \(c_x+c_y-dis(x,y)+\max_{z\in path(x,y)}c_z\),树剖查路径 \(\max\) 即可。
- \(z\) 不在路径上,但在 \(LCA(x,y)\) 的子树内,此时权值为 \(c_x+c_y+\max_{p\in path(x,y)}\max_{z\in subtree(p)}(c_z-2\times dis(p,z))\),\(\max_{z\in subtree(p)}(c_z-2\times dis(p,z))\) 可以树形 DP 预处理出来,树剖查路径 \(\max\) 即可。
- \(z\) 不在 \(LCA(x,y)\) 的子树内,此时权值为 \(c_x+c_y+\max(c_z-2\times dis(LCA(x,y),c_z))\),为了方便,\(z\) 可以在树上任选,重复的情况计算出来不会更优,所以后面可以把一个查询挂在 \(LCA(x,y)\) 上,换根 DP 的时候进行处理。
然后就……完了,目前做过的比较重工业的一道 DP 了,但出乎意料的没打多久且一遍过。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const ll N=2e5+5,INF=0x3f3f3f3f3f3f3f3f;
struct segment_tree_node{
ll ma;
void give_start_val(ll x){ma=x;}
friend segment_tree_node merge(segment_tree_node x,segment_tree_node y){return {max(x.ma,y.ma)};}
};
const segment_tree_node NOTHING={-0x3f3f3f3f3f3f3f3f};
template<typename T,typename ST,int N> struct segment_tree{
T t[N<<2],Nothing;
segment_tree(T No){Nothing=No;}
inline int ls(int x){return x<<1;}
inline int rs(int x){return x<<1|1;}
#define mid ((l+r)>>1)
inline void push_up(int x){t[x]=merge(t[ls(x)],t[rs(x)]);}
void build(int p,int l,int r,ST* val){
if(l==r) return t[p].give_start_val(*(val+l)),void();
else{
build(ls(p),l,mid,val);
build(rs(p),mid+1,r,val);
push_up(p);
}
}
void change(int p,int l,int r,int pos,ST val){
if(l==r) return t[p].give_start_val(val),void();
else{
if(pos<=mid) change(ls(p),l,mid,pos,val);
else change(rs(p),mid+1,r,pos,val);
push_up(p);
}
}
T query(int p,int l,int r,int re_l,int re_r){
if(re_l<=l&&r<=re_r) return t[p];
else if(!(r<re_l||l>re_r)) return merge(query(ls(p),l,mid,re_l,re_r),query(rs(p),mid+1,r,re_l,re_r));
else return Nothing;
}
};
segment_tree<segment_tree_node,ll,N>T(NOTHING),Tn(NOTHING);
struct edge{ll to,w;};
struct query{ll u,v,num;};
int n,m,dep[N],fa[N],top[N],wa[N],siz[N],dfn[N],dfc;
ll d[N],a[N],dp[N],ma[N],ans[N];
vector<edge>e[N];
vector<query>q[N];
void dfs1(int now,int f){
siz[now]=1;fa[now]=f;
for(auto it:e[now]){
if(it.to!=f){
dep[it.to]=dep[now]+1,d[it.to]=d[now]+it.w;
dfs1(it.to,now);
siz[now]+=siz[it.to];
if(siz[it.to]>siz[wa[now]]) wa[now]=it.to;
}
}
}
void dfs2(int now,int f,int Top){
dfn[now]=++dfc,top[now]=Top;
if(wa[now]) dfs2(wa[now],now,Top);
for(auto it:e[now]){
if(it.to!=f&&it.to!=wa[now]) dfs2(it.to,now,it.to);
}
}
int LCA(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
ll qmaxv(int x,int y){
ll ret=-INF;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ret=max(ret,Tn.query(1,1,n,dfn[top[x]],dfn[x]).ma);
x=fa[top[x]];
}
return max(ret,Tn.query(1,1,n,min(dfn[x],dfn[y]),max(dfn[x],dfn[y])).ma);
}
ll qmaxdp(int x,int y){
ll ret=-INF;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ret=max(ret,T.query(1,1,n,dfn[top[x]],dfn[x]).ma);
x=fa[top[x]];
}
return max(ret,T.query(1,1,n,min(dfn[x],dfn[y]),max(dfn[x],dfn[y])).ma);
}
ll dis(int x,int y){return d[x]+d[y]-2*d[LCA(x,y)];}
void DP(int now,int f){
dp[now]=a[now];
for(auto it:e[now]){
if(it.to!=f){
DP(it.to,now);
dp[now]=max(dp[now],dp[it.to]-2*it.w);
}
}
}
void get_ans(int now,int fa,ll w){
ma[now]=dp[now];
if(fa) ma[now]=max(ma[now],ma[fa]-2*w);
for(auto it:q[now]) ans[it.num]=max(ans[it.num],a[it.u]+a[it.v]+ma[now]-dis(it.u,it.v));
for(auto it:e[now]){
if(it.to!=fa) get_ans(it.to,now,it.w);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(ll i=1,u,v,w;i<n;i++){
cin>>u>>v>>w;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
memset(dp,-0x3f,sizeof(dp));
dfs1(1,0),dfs2(1,0,1),DP(1,0);
for(int i=1;i<=n;i++) T.change(1,1,n,dfn[i],dp[i]),Tn.change(1,1,n,dfn[i],a[i]);
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;ans[i]=-INF;
ans[i]=max(ans[i],a[u]+a[v]+qmaxv(u,v)-dis(u,v));
ans[i]=max(ans[i],a[u]+a[v]+qmaxdp(u,v)-dis(u,v));
q[LCA(u,v)].push_back({u,v,i});
}
get_ans(1,0,0);
for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
}

浙公网安备 33010602011771号