To_Heart—题集——和她一同旋转的还有我的未来
1.ARC101E
link && submission
一直认为容斥都是人类智慧题
容斥!定义 f[s] 表示 s 集合内部的边一定没有被染色。那么答案就是 \(\sum (-1)^k f[s]\),其中 k 是 s 的 size。
没有染色说明什么?不妨假设一条边 \(i \in s\) ,那么它连接的两个点肯定是不联通的。这里联通的定义是不能通过不在 s 里的边到达。
因为这里我们不用管每个联通块内部的状态,只需要求出每组容斥对应的状态,所以定义 g(i) 表示大小为 i 的连通块任意配对的方案数。如果 i 是奇数那么 g(i)=0,否则 $g(i)=\prod \limits_{j=1} ^{\frac{i}{2}} (2j-1) $ 考虑意义的话就是你每次考虑把当前没有染色过的最小的点拿出来,再为其随便选一个点。
考虑 dp 。定义 dp[i][j] 子树 i 内节点 i 所在的连通块大小为 j 的容斥系数。答案就是 \(\sum dp[1][i] \times g(i)\)。
现在来看怎么转移。如果当前枚举这条边不断掉,那么就是一个树形背包;如果当前这条边断掉了?由乘法原理得 \(dp[x][i] \leftarrow - dp[x][i] \times dp[y][j] \times g(j)\) 因为是容斥所以有负号,你相当于是在把很多个子集一起算。
然后做完了。
2.CF1854C
link && submission
发现自己是概率垃圾数学垃圾信竞耻辱/kk/kel/wq/ll/dk
把数轴上初始存在的数看作关键点,首先根据期望的线性,我们将问题转换成每一个点被删去的期望步数。然后我们把一个点追逐上另一个点之后的过程看作删去追逐另一个点的那个点,也就是两点中靠前的那个点。
定义 dp[i][j] 表示追逐的点初始位置在 i,被追逐的点初始位置在 j 时删去追逐的点的期望步数。这样定义的好处是我们不用考虑数轴上 i 之前的点发生了什么。只考虑这一对点,每个点都有一半的概率被选中。那么 $dp[i][j] = \frac{1}{2}(dp[i+1][j] +1) + \frac{1}{2} dp[i][j+1] $。
最终的答案是 \(m-s_n+1 + \sum\limits_{i=1}^{n-1} dp[s_i][s_{i+1}]\) 。
3.AGC023F
link && submission
想到了很久以前想对 kww 说的一句话:你就像一棵树的根节点!
如果我们可以随便选点,那么最优的方案就是排成 00000111111 但是这道题并不像 kww 一样脑瘫。考虑怎么把问题往 kww 上面靠。发现最后的序列一定是在上述序列上选择了几对 01 交换位置。那么在选点的时候我们先把 0 选了无论如何都是不亏的,所以如果一个权值为 0 的点一旦可以选就一定会被选。我们考虑把这个信息和他父亲合并。然后把信息放在优先队列里面实时更新就做完了。
4.CF1515E
link && submission
这道题我真做过?哦去年做的啊那没事了。开胡!
如果所有电脑都是自己手动打开的一共有多少种方案数?枚举第一个打开的电脑,接下来分别有 \(\binom{n-1}{0},\binom{n-1}{1},……\binom{n-1}{n-1}\),共有 \(2^{n-1}\) 种选择方式。然后发现两堆手动打开的电脑中间最多夹一个自动打开的。利用这个性质分段然后 dp。 定义 dp[x][y] 表示前面 i 个电脑打开了,其中手动打开了 j 台电脑的方案数。那么转移显然是 \(dp[i+1+k][j+k]=dp[i][j]×2^{k-1}×C[j+k][k]\) 然后就做完了。时间复杂度 \(\mathrm{O(n^3)}\) 的。
看看代码。我抄的哪篇题解啊根本不对啊。
哦,连续段 dp!这下听懂了。
在一类序列计数问题中,状态转移的过程可能与相邻的已插入元素的具体信息相关。我们将整个序列通过还未插入数字的位置分成若干个连续段,那么显而易见的每次往空位置插入数字将会对当前的连通块状态产生三种影响,第一种是在中间多一个连续段,第二种是合并两个连续段,第三种是延长一个连续段。理解 连续段dp 的难点在于如何考虑其所谓的 “不需要考虑两个连续段之间的绝对位置”,只需要考虑其相对位置。
为什么这个是对的?因为你每次插入一定能对应原序列的一种插入方案。不存在插入方案也就不存在转移了。所以一定是对的。
然后我的代码就很好理解了。但是一般来说此类 dp 应该去刷表来做,会清晰很多。但是我做这道题的时候被 kww 附身了,脑瘫了。
5.CF908D
link && submission
推式子题。但是我不想推怎么办呢。这道题还需要认真读题。
他要什么我就设什么,定义状态 dp[i][j] 表示当前至少有了 i 个 a,j 个 ab 的期望步数。很明显的状态转移式 \(dp[i][j]=A\times dp[i+1][j] + B \times dp[i][j+1]\),其中 \(A/B\) 表示下一步选择 a/b 的概率。答案为 dp[1][0](因为一开始你随机出来多少个b都不影响最终 ab 的个数)。
然后推式子推出来的东西大概是:当 \(i+j \geq k\) 的时候,\(dp[i][j]=i+j+\frac{pa}{pb}\)。
这个推起来很麻烦!大概就是你考虑当前已经达到了 \(i+j+1=k\) 的时候,下一步如果选择了 b 那么就停止了,否则就会多一对 ab。所以期望可以写成 \(B\times \sum_{l=0} (i+j+l) * A^l\)。然后你发现后面是个无穷的等比数列,然后化一化再忽略忽略无穷项就好了(雾)。
6.CF1251E2
link && submission
挺反常识的一道题。我们将所有人按照 \(m_i\) 降序排列,然后把每个人放入优先队列延迟删除。删除的条件是当前遍历的人不满足没说动。
反常识是因为我一开始的想法是按照费用升序排列再做。怎么解决到底选择哪一类作为排序条件呢?现在比较直观的一种想法是看他的限制在哪里。如果限制在 a 上就排序 a之类的。
7.luoguP8476
又是一类神仙套路!
考虑一些比较显然的 n^2 做法。将 a[i] 离散化,然后定义 dp[i][j] 表示考虑到 i 且 b[i]=j 的最小价值。转移显然。考虑优化。
我们发现 dp 在第二维呈现一个后缀最小值的趋势,所以整个序列 dp[i] 以函数形式呈现是单调不减的。然后再考虑由 dp[i]->dp[i+1] 的更新过程。发现 j=a[i] 以前的 j 和往后的 i 是两种不同的更新状态。把这个放在线段树上,前面一段相当于给 [1,a[i]-1] 的区间加上一个序列 b;后面就是区间加了。
有个细节是你在 a[i] 前后加的东西不一样可能会导致线段树上的函数不满足单调。所以你需要找到那一段不满足的然后区间赋值。
这道题就做完了。三种修改,一种查询,一种线段树二分。没了。
我们来总结一下这一类题目的共同点。有个屁的共同点,就是函数分成常数段后每一段的转移是相同的。而且应该是要满足某种性质使得转移点固定或者只有常数个。
小孩子不懂事乱总结的。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
int lsh[1000005],tot=0;
int a[1000005];
int n,c;
struct zz{
int l,r;
ll Max,Min;
ll add,tot,tag;
};
struct Tree{
#define lc p<<1
#define rc p<<1|1
zz t[4000005];
void Push_Down(int p){
if(t[p].tag!=-1){
t[lc].Max=t[lc].Min=t[rc].Max=t[rc].Min=t[p].tag;
t[lc].add=t[rc].add=t[lc].tot=t[rc].tot=0;
t[lc].tag=t[rc].tag=t[p].tag;
t[p].tag=-1;
}
if(t[p].add){
t[lc].Max+=t[p].add,t[rc].Max+=t[p].add;
t[lc].Min+=t[p].add,t[rc].Min+=t[p].add;
t[lc].add+=t[p].add,t[rc].add+=t[p].add;
t[p].add=0;
}
if(t[p].tot){
t[lc].Max+=lsh[t[lc].r]*t[p].tot,t[rc].Max+=lsh[t[rc].r]*t[p].tot;
t[lc].Min+=lsh[t[lc].l]*t[p].tot,t[rc].Min+=lsh[t[rc].l]*t[p].tot;
t[lc].tot+=t[p].tot,t[rc].tot+=t[p].tot;
t[p].tot=0;
}
}
void Push_Up(int p){ t[p].Min=min(t[lc].Min,t[rc].Min); t[p].Max=max(t[lc].Max,t[rc].Max);};
void Build_Tree(int p,int l,int r){
t[p].l=l,t[p].r=r,t[p].Max=t[p].add=t[p].tot=0,t[p].tag=-1,t[p].Min=0x3f3f3f3f3f3f3f3f;
if(l==r) return ;
int mid=(t[p].l+t[p].r)>>1;
Build_Tree(lc,l,mid),Build_Tree(rc,mid+1,r);
Push_Up(p);
}
void Change_Tree_1(int p,int l,int r,int val){
if(l>r) return ;
if(l<=t[p].l&&t[p].r<=r) return t[p].Min+=val,t[p].Max+=val,t[p].add+=val,void();
Push_Down(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid) Change_Tree_1(lc,l,r,val);
if(mid+1<=r) Change_Tree_1(rc,l,r,val);
Push_Up(p);
}
void Change_Tree_2(int p,int l,int r,int tot){
if(l>r) return ;
if(l<=t[p].l&&t[p].r<=r) return t[p].Min+=lsh[t[p].l]*tot,t[p].Max+=lsh[t[p].r]*tot,t[p].tot+=tot,void();
Push_Down(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid) Change_Tree_2(lc,l,r,tot);
if(mid+1<=r) Change_Tree_2(rc,l,r,tot);
Push_Up(p);
}
void Change_Tree_3(int p,int l,int r,int val){
if(l>r) return ;
if(l<=t[p].l&&t[p].r<=r) return t[p].Min=t[p].Max=val,t[p].add=t[p].tot=0,t[p].tag=val,void();
Push_Down(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid) Change_Tree_3(lc,l,r,val);
if(mid+1<=r) Change_Tree_3(rc,l,r,val);
Push_Up(p);
}
int Find_Tree_1(int p,int pos){
if(t[p].l==t[p].r) return t[p].Max;
Push_Down(p);
int mid=(t[p].l+t[p].r)>>1;
if(pos<=mid) return Find_Tree_1(lc,pos);
else return Find_Tree_1(rc,pos);
}
int Find_Tree_2(int p,int l,int r,int val){
if(l>r) return 0;
if(t[p].Max<val) return 0;
if(t[p].l==t[p].r) return t[p].Min>=val?t[p].l:0;
Push_Down(p);
int mid=(t[p].l+t[p].r)>>1;
int pos=0;
if(l<=mid) pos=Find_Tree_2(lc,l,r,val);
if(!pos) if(mid+1<=r) pos=Find_Tree_2(rc,l,r,val);
return pos;
}
}T;
signed main(){
// freopen("wave.in","r",stdin);
// freopen("wave.out","w",stdout);
cin>>n>>c;
for(int i=1;i<=n;i++) scanf("%lld",&a[i]),lsh[++tot]=a[i];
sort(lsh+1,lsh+tot+1);tot=unique(lsh+1,lsh+tot+1)-(lsh+1);
for(int i=1;i<=n;i++) a[i]=lower_bound(lsh+1,lsh+tot+1,a[i])-(lsh);
T.Build_Tree(1,1,n);
for(int i=1;i<=n;i++){
T.Change_Tree_1(1,1,a[i]-1,c),T.Change_Tree_1(1,a[i]+1,tot,-lsh[a[i]]);
T.Change_Tree_2(1,a[i]+1,tot,1);
int val=T.Find_Tree_1(1,a[i]);
int pos=T.Find_Tree_2(1,1,a[i],val);
if(pos!=0) T.Change_Tree_3(1,pos,a[i],val);
}
printf("%lld\n",T.t[1].Min);
return 0;
}
8.ARC076F
link && submission
一眼二分图,考虑模拟匹配过程。首先我们假设所有的人都往他靠左的区间坐,坐不下了就在所有位置中找到一个 r最小的把他放在右边去,这个人补上去。这个过程可以反悔贪心。
但其实这道题还可以 hall 定理来做。后面来补?
9.CF1789F
脑瘫题。哦不,kww 题。 把分治思想发挥到极致的一道题
考虑做朴素的暴力,大概就是枚举 k-1 个断点,然后对分出来的 k 个连续段跑最长公共子序列。时间复杂度 \(\mathrm{O(n^{2k-1})}\)。 考虑数据范围,发现可以跑出 k<=3 的答案。
然后再考虑 k 比较大的情况。有一个非常关键的性质,就是一定存在一个子序列 T' 满足其在原序列的跨度不会超过 n/k 。如果都超过了就不存在 k 次循环下的 T' 了。根据这个性质我们可以枚举每段 n/k 然后暴力处理处 T' 看看能不能凑出来 k 个。时间复杂度为 \(\mathrm{O(n^2 2^{\frac{n}{k}})}\) 可以跑过 k>=5 的点。
对于 k=4 的点呢?发现 k=4 就是 k=2 的 T'T' 已经被判断过了。所以做完了。
10.luoguP1527
整体二分板子。
整体二分需要注意什么?
- 确定二分的是什么?
- 确定代价应该怎么写。
#include<bits/stdc++.h>
using namespace std;
int n,q;
struct zz{
int x,y;
int xx,yy;
int val;
int id;
}t[1000005];
bool cmp(zz x,zz y){
return x.id<y.id;
}
vector<zz> v;
int a[1005][1005];
int ans[1000005];
struct Tree{
int bit[1005][1005];
int Lowbit(int x){ return x&(-x); }
void Change(int x,int y,int val){ for(int i=x;i<=n;i+=Lowbit(i)) for(int j=y;j<=n;j+=Lowbit(j)) bit[i][j]+=val; }
int Find(int x,int y){ int ans=0; for(int i=x;i;i-=Lowbit(i)) for(int j=y;j;j-=Lowbit(j)) ans+=bit[i][j]; return ans; }
int Find_Tree(int x,int y,int xx,int yy){ return Find(xx,yy)-Find(x-1,yy)-Find(xx,y-1)+Find(x-1,y-1); }
}T;
void Solve(int l,int r,vector<zz> q){
if(!q.size()) return ;
if(l==r){ for(auto now:q) ans[now.id]=t[l].id; return ; }
int mid=(l+r)>>1;
for(int i=l;i<=mid;i++) T.Change(t[i].x,t[i].y,1);
vector<zz> q1,q2;
for(auto now:q){
if(now.val<=T.Find_Tree(now.x,now.y,now.xx,now.yy)) q1.push_back(now);
else now.val-=T.Find_Tree(now.x,now.y,now.xx,now.yy),q2.push_back(now);
}
for(int i=l;i<=mid;i++)T.Change(t[i].x,t[i].y,-1);
Solve(l,mid,q1),Solve(mid+1,r,q2);
}
int main(){
cin>>n>>q;
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) t[(i-1)*n+j]=(zz){i,j,0,0,0,a[i][j]};
sort(t+1,t+n*n+1,cmp);
for(int i=1,x,y,xx,yy,z;i<=q;i++) scanf("%d%d%d%d%d",&x,&y,&xx,&yy,&z),v.push_back((zz){x,y,xx,yy,z,i});
Solve(1,n*n,v);
for(int i=1;i<=q;i++) printf("%d\n",ans[i]);
return 0;
}