每日总结 (一)
8.31
P5665划分 :
算法:贪心,单调队列
首先是一个数学知识 \(a^2+b^2\le(a+b)^2\) 所以我们应该尽可能地多分段,当然在保证分成的段单调递增的前提下,也应该尽可能地把数分给小的那一段,然后开 __int128 多取模。
注意 : 取模时应该注意得数的正负如果是负的应该先加 \(mod\) 然后再模 \(mod\). 提前计算好运算的上界,决定是开\(int\)还是开\(long long\) 还是 __int128(__int128 读入只能用快读,输出只能用快写)。
P5021赛道修建 :
算法:二分答案,贪心,求LCA
贪心思路:找一个 \(a_i\) 找到第一个不小于 \(l-a_i\) 的数 \(a_j\) 去除这两个数,记录答案,若没找到则跳 \(a_i\) 的后继
证明:
1.最优匹配中包含 \(a_x\) : 则应该选择 \(a_y\),因为选择小于它的元素都不满足条件。选择大于它的元素的情况,选择 \(a_y\) 对其是具有决
策包容性的。2.最优匹配中不包含 \(a_x\) : 则最优排列中必然包含 \(a_y\),否则还有未统计的匹配对 \((a_x,a_y)\)。设 \(a_y\) 与 \(a_z\) 匹配,显然 \(a_z≥a_x\) 。那么这个匹配可能不是最优,或与选 \(a_x\) 等效。
P7078贪吃蛇 :
算法:贪心
结论:只需模拟两个阶段:
一:所有最强蛇进食后都不是最弱蛇,放心大胆吃!
二:所有最强蛇进食后都是最弱蛇,直到有条蛇可以放心吃为止(吃了后不是最弱或者只剩两条)
注意:不需要开 \(long long\) 的题不要盲目开,注意常数优化。
9.1
P8632居民集会
算法: 模拟退火,斜率优化DP
\(DP[i][j]\) 表示在第 \(i\) 个居民设置第 \(j\) 个集会点,前 \(i\) 个居民的最小值, \(st\)是\(t\)的前缀和数组,\(std\)是 \(t*d\) 的前缀和数组,转移DP式子就是:
\[dp_{i,j}=min_{k=0}^{i-1} (dp_{k,j-1}+d_i*(st_i-st_k)-(std_i-std_k)) \]然后套个斜率优化
注意:斜率优化的实现形式。
P7116微信步数
算法:动态规划DP 拉格朗日插值
处理出来横竖的延伸长度,先处理两轮的,然后再在这两轮上扩展。
注意 : 取模时应该注意得数的正负如果是负的应该先加 \(mod\) 然后再模 \(mod\). 提前计算好运算的上界,决定是开\(int\)还是开\(long long\) 还是 __int128(__int128 读入只能用快读,输出只能用快输),多取模。
9.2:
b
算法:数学知识的应用,\(k\) 优解
首先看到要比较两个特别大的组合数,我们可以在上面套个 \(log\) 然后组合数的大小就变的可以接受了,可以拿 \(long double\) 存下来。
然后就是 \(k\) 优解问题打个表观察发现,把组合数写成三角的形式,他是一个由两边向四周的辐射状,然后拿优先队列维护即可。
c
算法:动态规划DP
设置 \(f_{i,j,0/1}\) 表示长为 \(i\) 的序列,在后面加 \(j\) 种数就会变合法,\(0/1\) 表示当前状态是否合法。
状态转移式子为:
\[dp[i+1][j][1]+=dp[i][j][1]*j \]\[dp[i+1][j+1][0]+=dp[i][j][1]*(m-j) \]\[dp[i+1][j][0]+=dp[i][j][0]*(m-j) \]\[dp[i+1][j][1]+=dp[i][j][0]*j \]
d
算法:主席树
考虑对一个人 \(i\) 有贡献的事件 \(k\) 为 \(s_k-s_i\) 的一个上升序列。
即一个 \(s_i\) 的上升序列,对于每一个点指向后面第一个比它大的数,形如一棵树,它到路径上的点即为贡献事件集。
求两点 \(lca\) 即可得到对两点均有贡献的事件,最后组主席树统计答案即可。
9.3
点击查看
今天是抗战胜利80周年。上午看了阅兵
P4001 狼抓兔子
算法:图论建模(平面图转对偶图), 最短路
观察题面,题面给了一个网格图,还带边权,显然是最小割,可以转成对偶图然后跑最短路,跑完之后就是最小割,具体的:给每个格子编号,确保网格图中的边每条都恰好被一条对偶图中的边切,对偶图中的边权就是所切的网格图中的边的边权
注意:看清对偶图中的终点的编号是什么!!!
P1850 换教室
算法:期望DP
设 \(DP_{i,j,1/0}\) 表示前 \(i\) 间教室换了 \(j\) 间,第 \(i\) 间教室 (是/否) 换了。
转移式子:
\[dp_{i,j,0}=min\left\{\begin{matrix} dp_{i-1,j,0}+dis_{c[i-1],c[i]} \\ dp_{i-1,j,1}+(1-k_i)\times dis_{c[i-1],c[i]} + k_i \times dis_{c[i-1],d[i]} \end{matrix}\right. \]
\[dp_{i,j,1}=min\left\{\begin{matrix} dp_{i-1,j-1,0}+(1-k_i)\times dis_{c[i-1],c[i]} + k_i \times dis_{c[i-1],d[i]} \\ \\ dp_{i-1,j-1,1}+\\ (1-k[i-1])\times (1-k[i])\times dis[c[i-1]][c[i]]+\\ (1-k_{i-1})\times k_i\times dis_{c[i-1],d[i]}+\\ k[i-1]\times (1-k[i])\times dis_{d[i-1],c[i]}+\\ k[i-1]\times k[i]\times dis_{d[i-1],d[i]} \end{matrix}\right. \]
注意:\(Floyd\) 要先枚举中转点 \(k\) 。
P3960 列队
算法:线段树,平衡树,树状数组,块状链表。
这道题可以用 线段树/平衡树/树状数组 来写,我写的动态开点线段树。
首先观察题面,一个人 \((x,y)\) 出队再回队,就是把 \(x\) 这一行的第 \(y\) 个人删了全体左移,然后把最后一列第 \(x\) 个人删了,整体上移,然后把 \((x,y)\) 这个人放在最后一列最后一个。
我们只需要对每一行和最后一列分别维护一颗线段树,动态维护就可以了,但是我们一算这样的空间复杂度是 \(O(n^2 log_2n)\) 的。
显然需要优化,我们发现没有出过队的人没什么用,我们就只对出过队的人建立与他相关的节点,并记录它前面删过的点的个数,直接统计即可
注意:对于题面的转化,和知识的应用。
9.4
P4138 挂饰
算法:动态规划DP,背包DP
设 \(dp[i][j]\) 表示前 \(i\) 个物品,还剩 \(j\) 个挂钩,可以获得的最大价值,
状态转移方程:
$ if(j>=pos[i].a)$
\[dp[i][j]=max(dp[i-1][j],dp[i-1][j-pos[i].a+1]+pos[i].b) \]$ else $
\[dp[i][j]=max(dp[i-1][j],dp[i-1][1]+pos[i].b) \]答案为 $$min_{i=0}^{n} dp[n][i]$$
注意 :在转移之前应该先排序,确保有挂钩最多的先遍历
P5023 填数游戏
算法:打表,动态规划DP
看到 \(n\) 的范围很小,然后和出题人的脑电波同频共振,然后再被宇宙射线击中一下,再打个表.
寻找一下规律发现:\(a_{i,j}=a_{i,i+1} \times 3^{j-i-1}\) ,\(n\) 很小可以打个有剪枝的暴搜,让它辛苦一下,跑一个 \(8\times 9\) 的表然后 \(O(nm)\) 预处理,\(O(1)\) 查询。
注意:数组很大时不能直接在定义时拿 { } 定义编译时 \(C++\) 会自动补全,本地运行没事,测评时会 \(CE\).
P4751 动态DP
算法:动态DP,树链剖分,线段树
动态DP模板题,首先暴力 \(DP\) :设 \(DP_{i,0/1}\) 表示到 \(i\) 号节点是否选它,转移式子也很显然,然后把轻重儿子分开考虑设 \(g_{i,0/1}\) 表示 \(i\) 号节点所有轻儿子所能构成的最大权独立集。
转移方程:
\[f_{i,0}=g_{i,0}+max(f_{j,0},f_{j,1}) \]\[f_{i,1}=g_{i,1}+a_i+f_{j,0} \]然后构建一个转移矩阵
\[\begin{bmatrix} g_{i,0} & g_{i,0} \\ g_{i,1} & -\infty \end{bmatrix} \times \begin{bmatrix} f_{j,0} \\ f_{j,1} \end{bmatrix} = \begin{bmatrix} f_{i,0} \\ f_{i,1} \end{bmatrix} \]最后发现更改一个点的权值只会改这条链的链头的父亲,直接 就是 一个 线段树维护区间。
注意:数组大小要开够
P5024 保卫王国
算法:动态DP
先推个式子,和模板题长得很像,然后搓个矩阵,强制选或不选只需要在转移矩阵的对应位置赋成 \(inf\) 确保是极大值只能选另一个即可
剩下的就和板子题一样了。
9.5
P10237 Latent Kindom
算法:线段树,二分
看到值域 \(1e18\) 先离散化,然后对每个序列建动态开点线段树,并维护他们的大小。查询答案的时候两棵树一起跑,然后在线段树上二分即可。
注意:看清没个变量对应的是什么,注意 \(+1 / -1\) 的细节处理。
P5666 树的重心
算法:树形 \(DP\),树状数组
首先对于删去一条边后的一棵子树的大小 \(S\) 应该满足:
\[n-2 * s_x\le S\le n-2 * g_x \]其中 \(g_x\) 为 \(x\) 的儿子中子树最大的大小。
然后分别用两个树状数组维护 \(S\) ,然后发现最后就是一个区间求和。
注意:清空一定要清全
P7963 棋局
算法:并查集,线段树
无需多言,请吧:
点击查看
#include<bits/stdc++.h>
#define int long long
const int N = 2e5+10, M = 8e6+10;
using namespace std;
inline int read() {
int x=0,fa=1;char ch=getchar();
while(ch<'0'||ch>'9') fa=ch=='-'?-1:1,ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*fa;
}
int T_T,n,m,Q,val[N],e[4][N],ans[N]; string s;
int dx[4]= {-1,0,1,0},dy[4]= {0,-1,0,1};
struct edge { int col,lv,x,y,id;}a[N];
inline bool cmp1(edge a,edge b) {return (a.lv==b.lv)?(a.id<b.id):(a.lv<b.lv);}
inline bool cmp2(edge a,edge b) {return a.id<b.id;}
inline int pos1(int x,int y) {return (x-1)*m+y;}
inline int pos2(int x,int y) {return (y-1)*n+x;}
pair<int,int> Get(int pos) {return make_pair((pos-1)/m+1,(pos-1)%m+1);}
struct Tree1 {
#define mid ((l+r)>>1)
struct node {int ls,rs,sz;}tr[M]; int rt[N],cnt;
int ls(int x) {return tr[x].ls;} int rs(int x) {return tr[x].rs;}
inline void init() {memset(rt,0,sizeof rt);memset(tr,0,sizeof(tr));cnt=0;}
inline void push_up(int i) {tr[i].sz=tr[ls(i)].sz+tr[rs(i)].sz;}
inline int update(int i,int l,int r,int x,int opt=1) {
i=!i?++cnt:i;
if(l==r) return opt==-1?0:(tr[i].sz+=(!tr[i].sz),i);
if(x<=mid) tr[i].ls=update(ls(i),l,mid,x,opt);
else tr[i].rs=update(rs(i),mid+1,r,x,opt);
return push_up(i),(tr[i].sz)?i:0;
}
inline int merge(int x,int y,int l,int r) {
if(x==0||y==0) return x|y;
if(l==r) return tr[x].sz=min(tr[x].sz+tr[y].sz,1ll),x;
tr[x].ls=merge(ls(x),ls(y),l,mid);
tr[x].rs=merge(rs(x),rs(y),mid+1,r);
return push_up(x),x;
}
inline int query(int i,int l,int r,int x) {
if(i==0||x==0) return 0; if(l==r) return tr[i].sz;
if(x<=mid) return query(ls(i),l,mid,x);
else return tr[ls(i)].sz+query(rs(i),mid+1,r,x);
}
inline int ask(int i,int l,int r,int x) {
if(i==0||x==0) return 0;
if(l==r) return tr[i].sz;
if(x<=mid) return ask(ls(i),l,mid,x);
else return ask(rs(i),mid+1,r,x);
}
inline int ask_num(int i,int l,int r) {return query(rt[i],1,n*m,r)-query(rt[i],1,n*m,l-1);}
#undef mid
}T[4];
inline void Merge(int x,int y) {
T[0].rt[x]=T[0].merge(T[0].rt[x],T[0].rt[y],1,Q);
T[1].rt[x]=T[1].merge(T[1].rt[x],T[1].rt[y],1,Q);
T[2].rt[x]=T[2].merge(T[2].rt[x],T[2].rt[y],1,n*m);
T[3].rt[x]=T[3].merge(T[3].rt[x],T[3].rt[y],1,n*m);
}
struct Tree2 {
int fa[N],sz[N],ma[N],mi[N];
inline void init(int p) {for(int i=0;i<=p;i++) fa[i]=ma[i]=mi[i]=i,sz[i]=1;}
inline int find(int x) {return (fa[x]==x)?x:find(fa[x]);}
inline void merge(int x,int y,int opt=0) {
x=find(x),y=find(y); if(x==y) return;
if(sz[x]<=sz[y]) swap(x,y);
sz[x]+=sz[y]; fa[y]=x;
ma[x]=max(ma[x],ma[y]);
mi[x]=min(mi[x],mi[y]);
if(opt) Merge(x,y);
}
}Tr[3];
inline bool check(int x,int y) {return (y==0||y>=x)?0:(a[x].col!=a[y].col&&a[x].lv>=a[y].lv);}
inline void init() {
memset(e,0,sizeof(e)); memset(val,0,sizeof(val)); memset(ans,0,sizeof(ans));
Tr[0].init(n*m); Tr[1].init(n*m); Tr[2].init(n*m);
T[0].init();T[1].init();T[2].init();T[3].init();
}
inline void solve() {
n=read();m=read();Q=read(); init();
for(int i=1;i<=n;i++) {
cin>>s; s=' '+s;
for(int j=1;j<m;j++) e[1][pos1(i,j+1)]=e[3][pos1(i,j)]=s[j]-'0';
}
for(int i=1;i<n;i++) {
cin>>s; s=' '+s;
for(int j=1;j<=m;j++) e[0][pos1(i+1,j)]=e[2][pos1(i,j)]=s[j]-'0';
}
for(int i=1;i<=Q;i++) a[i]={read(),read(),read(),read(),i};
sort(a+1,a+Q+1,cmp1);
for(int i=1;i<=Q;i++) a[i].lv=i;
sort(a+1,a+Q+1,cmp2);
for(int i=1;i<=Q;i++) val[pos1(a[i].x,a[i].y)]=i;
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
int id=pos1(i,j);
for(int k=0;k<4;++k) {
if(e[k][id]>1) {
int nxt=pos1(i+dx[k],j+dy[k]);
if(!val[id]&&!val[nxt])
Tr[(e[k][id]==3)?0:(k%2+1)].merge(id,nxt);
}
}
}
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
int id=pos1(i,j),Id=Tr[0].find(id);
T[2].rt[Id]=T[2].update(T[2].rt[Id],1,n*m,pos2(i,j));
T[3].rt[Id]=T[3].update(T[3].rt[Id],1,n*m,id);
for(int k=0;k<4;++k) {
if(e[k][id]==3) {
int nxt=pos1(i+dx[k],j+dy[k]);
if(val[nxt]) {
int col=a[val[nxt]].col;
T[col].rt[Id]=T[col].update(T[col].rt[Id],1,Q,a[val[nxt]].lv);
}
}
}
}
}
for(int i=Q;i>=1;--i) {
int id=pos1(a[i].x,a[i].y),col=a[i].col;
for(int j=0;j<4;j++) {
if(e[j][id]==3) {
int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]),Fnxt=Tr[0].find(nxt);
T[col].rt[Fnxt]=T[col].update(T[col].rt[Fnxt],1,Q,a[i].lv,-1);
}
}
for(int j=0;j<4;j++) {
if(e[j][id]==3) {
int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
if(val[nxt]&&val[nxt]<i) continue;
Tr[0].merge(id,nxt,1);
}
}
int Id=Tr[0].find(id);
ans[i]=Tr[0].sz[Tr[0].find(id)]+T[1^col].query(T[1^col].rt[Id],1,Q,a[i].lv);
for(int j=0;j<4;j++) {
if(e[j][id]==2) {
int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
if(val[nxt]&&val[nxt]<i) continue;
Tr[j%2+1].merge(id,nxt);
}
}
int mx1=Tr[1].ma[Tr[1].find(id)],mx2=Tr[2].ma[Tr[2].find(id)],mn1=Tr[1].mi[Tr[1].find(id)],mn2=Tr[2].mi[Tr[2].find(id)];
ans[i]+=mx2-mn2+1+(mx1-mn1)/m+1;
int dmx=pos2(Get(mx1).first,Get(mx1).second),dmn=pos2(Get(mn1).first,Get(mn1).second);
ans[i]-=T[2].ask_num(Id,dmn,dmx)+T[3].ask_num(Id,mn2,mx2);
if(e[0][mn1]==2&&check(i,val[mn1-m])) ans[i]+=(!T[1^col].ask(T[1^col].rt[Id],1,Q,a[val[mn1-m]].lv));
if(e[1][mn2]==2&&check(i,val[mn2-1])) ans[i]+=(!T[1^col].ask(T[1^col].rt[Id],1,Q,a[val[mn2-1]].lv));
if(e[2][mx1]==2&&check(i,val[mx1+m])) ans[i]+=(!T[1^col].ask(T[1^col].rt[Id],1,Q,a[val[mx1+m]].lv));
if(e[3][mx2]==2&&check(i,val[mx2+1])) ans[i]+=(!T[1^col].ask(T[1^col].rt[Id],1,Q,a[val[mx2+1]].lv));
for(int j=0;j<4;j++) {
if(e[j][id]==1) {
int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
if(val[nxt]&&val[nxt]<i) ans[i]+=(check(i,val[nxt])&&(T[1^col].ask(T[1^col].rt[Id],1,Q,a[val[nxt]].lv)==0));
else if(Tr[0].find(id)!=Tr[0].find(nxt)) ans[i]++;
}
} ans[i]--;
}
for(int i=1;i<=Q;i++) printf("%lld\n",ans[i]);
}
signed main() {T_T=read();while(T_T--) solve();return 0;}
9.6
A P3760
看到异或运算,考虑按位考虑贡献,用前缀和优化,化一下式子。
\[\sum_{k=0}^{maxbit} 2^k \sum_{l=0}^{n}\sum_{r=l+1}^{n} ((sum_r-sum_l)\mod 2) \]考虑减法的按位意义,发现与借位有关,分 0/1 的情况讨论,用树状数组维护即可。
C P5443
没有修改操作时是 kruscal 重构树裸题,
有了修改操作不是很好维护,考虑根号算法,根号重构,对每 \(sqrt{n}\) 个询问进行一次暴力重构,
将边按权值排序后,将无修改的边加入边集,修改的边将最新的权值加入,用可撤销并查集维护这个过程。
9.7
P3730 曼哈顿交易
算法:莫队,分块
看到题面是询问区间计数的相关,又没什么思路,线段树也不好维护,考虑莫队,
用分块维护相关信息,每次移动左右指针时,更新对应值,然后查询时用分块查询即可。
P10592 isn
算法:树状数组,组合计数,动态规划DP
设 \(dp_{i,j}\) 表示前 \(i\) 个数选 \(j\) 个并且选第 \(i\) 个数构成非降序列。 转移方程:
\[ dp_{i,j}=\left\{\begin{matrix} 1 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ j=1\\ \sum_{k=1}^{i-1} [a_k\le a_i] dp_{k,j-1} \ \ \ \ \ j \ne 1 \end{matrix}\right. \]用树状数组实现转移,然后 \(mp_i=\sum_{j=1}^{n} dp_{j,i}\)
用容斥知识 \(ans=\sum_{i=1}^{n} mp_i \times (n-i)! - mp_{i+1} \times (n-i-1)!\times(i+1)\)
P14005 棋盘游戏
算法:构造?
神仙结论题,输出什么都能A(?
可以证明无论初始黑点是什么,都可以通过一系列操作构造任意的形态。
9.8
P12685 排队 加强版
算法:树套树
树套树板子题,逆序对经典 \(Trick\) 两个数交换只有两个数中间的数会影响答案。
只需要初始算一遍,然后每次计算贡献的差值即可。
P10235 舞萌基本练习
算法:二分答案,树状数组
经过简单手玩,发现答案显然具有单调性,遂二分答案
然后用树状数组维护逆序对即可。
注意:清空能不暴力清,就不暴力清,顺着来路走回去也许会更好。
9.9
P10833 下 Niz
算法:分治
我们根据最值分治,最值确定了,区间长度也就确定了,设 \(las\) 为这个数上一次出现的位置,显然当一个区间合法时 \(las_l~las_r\) 都不能在当前区间出现。
显然对于 \(l\) 我们只需要判断 \(las\) 的最大值即可,用两个 \(ST\) 表分别维护原数组和 \(las\) 数组的 \(RMQ\) 就行了。
然后正常分治即可。
P7482 不条理狂诗曲
算法:分治,动态规划DP
首先最值分治,设:\(f_{l,r}\) 表示区间的最大值
\(fl_{i,0/1}\) 表示:\([i,mid]\) 不选/选 \(a_{mid}\) 的最大值,\(fr\) 同理,转移方程:
\[f_{l,r} =max(fl_{l,0}+fr_{r,1},fl_{l,1}+fr_{r,0},fl_{l,0}+fr_{r,0}) \]\[=max(fl_{l,0}+fr_{r,1},max(fl_{l,1},fl_{l,0})+fr_{r,0})) \]令 \(mp\) 为 \(fl_{i,0}\) 的前缀和
令 \(ms\) 为 \(max(fl_{l,1},fl_{l,0})+fr_{r,0})\) 的前缀和
那么对答案的贡献就为\(mp_{k-1}+(k-l) \times fr_i\) 和 \(ms_{mid} - ms_{k-1}+(mid-k+1)\times fr_{i,0}\)