乱写——CF杂题
作者:互联网
\(\text{CodeForces}\) 杂题乱写
动态规划
CF149D Coloring Brackets
这是一个区间 dp,我们需要在状态中存入当前区间左右的颜色,来判断关于“匹配”以及“相邻”的限制,预处理用栈找到每一对匹配括号。
之后考虑这样一个问题,一个括号序列中两个相邻且独立的序列,他们整体的方案数其实是各自方案数的乘积(只不过要排除“相邻”限制),于是转移就比较简单了。
那么来看“匹配”的限制,讨论当前区间最外层是否匹配,若匹配直接“剥洋葱”一样地把区间 \([l,r]\) 缩小到区间 \([l+1,r-1]\),判断 \(l\) 与 \(l+1\) 以及 \(r-1\) 与 \(r\) 的颜色并求和即可;若不匹配就找到 \(l\) 所匹配到的位置 \(p(l)\),从中间断开,之后就是两个独立子问题排除限制相乘即可。
这里我们采用记忆化搜索的手段,相比于正常循环的区间 dp 是将区间由小到大、端点由左及右地算出,记忆化搜索如同“光翼展开”,这对于我们讨论是否匹配的需要有很大的帮助,原因是从最大区间展开成若干个小区间的过程中,一定有 \(p(l)\in[l,r]\)。
点击查看代码
char s[maxn];
int p[maxn];
inline void get_pair(){
stack<int> st;
for(int i=1;i<=n;++i){
if(s[i]=='(') st.push(i);
else{
p[st.top()]=i,p[i]=st.top();
st.pop();
}
}
}
ll dp[1005][1005][3][3];
inline void f(int l,int r){
if(l+1==r) dp[l][r][0][1]=dp[l][r][0][2]=dp[l][r][1][0]=dp[l][r][2][0]=1;
else if(p[l]==r){
f(l+1,r-1);
for(int i=0;i<=2;++i){
for(int j=0;j<=2;++j){
if(j!=-1) dp[l][r][0][1]=(dp[l][r][0][1]+dp[l+1][r-1][i][j]*(j!=1))%mod;
if(j!=-2) dp[l][r][0][2]=(dp[l][r][0][2]+dp[l+1][r-1][i][j]*(j!=2))%mod;
if(i!=-1) dp[l][r][1][0]=(dp[l][r][1][0]+dp[l+1][r-1][i][j]*(i!=1))%mod;
if(i!=-2) dp[l][r][2][0]=(dp[l][r][2][0]+dp[l+1][r-1][i][j]*(j!=2))%mod;
}
}
}
else{
f(l,p[l]);
f(p[l]+1,r);
for(int i=0;i<=2;++i){
for(int j=0;j<=2;++j){
for(int k=0;k<=2;++k){
for(int o=0;o<=2;++o){
if(j&&k&&j==k) continue;
dp[l][r][i][o]=(dp[l][r][i][o]+dp[l][p[l]][i][j]*dp[p[l]+1][r][k][o]%mod)%mod;
}
}
}
}
}
}
CF509F Progress Monitoring
可以看出,伪代码是在求魔改过的 dfs 序,改动的地方是同一个节点的子树中,先遍历根结点编号小的。
由于每个子树互相独立,\(b\) 中某一段区间如果独立出来不会对总体的 dfs 序产生影响,也就是说可以化成子问题。
于是考虑区间 dp,这里要用到钦定的方法,由于答案 \(dp(1,n)\) 是区间 \([1,n]\) 且以 \(1\) 为根节点的方案数,我们定义状态也要与此相同。\(dp(l,r)\) 表示区间 \([l,r]\) 整体作为一颗子树且 \(l\) 为根的方案数。
那么初始状态 \(l=r\) 时,\(dp(l,r)=1\)。接着思考,既然 \(l\) 作为根结点,那么 \(l+1\) 就是第一颗子树的根,令第二棵子树的根为 \(k+1\),也就是把 \([l,r]\) 划分成了 \([l+1,k]\) 与 \([k+1,r]\)。由于是区间 dp,我们划分出第一、二个区间即可,剩余的划分已经在第二个区间内部进行过了(子问题中可以保证 \([l+1,k]\) 与 \([k+1,r]\) 自己内部所有根都编号递增,但是不保证整体是递增的,不如只考虑一个区间,也就是 \(k\) 一定是第一、二个区间的断点)。那么 \(dp(l,r)\) 结果是对于每个 \(k\in[l+1,r]\) 中方案数的乘积之和。
代入到状态的定义,一棵子树是区间 \([l+1,k]\) 以 \(l+1\) 为根,即 \(dp(l+1,k)\),另外剩余的实质上是若干棵子树,即区间 \([k+1,r]\) 的森林,其实就是 \(dp(k,r)\) 的定义(有没有根对答案没有影响),于是转移方程:
点击查看代码
int n,b[505];
ll dp[505][505];
inline ll f(int l,int r){
if(l==r) return dp[l][r]=1;
if(dp[l][r]) return dp[l][r];
for(int k=l+1;k<=r;++k){
if(k==r) dp[l][r]=(dp[l][r]+f(l+1,r))%mod;
else if(b[l+1]<b[k+1]) dp[l][r]=(dp[l][r]+f(l+1,k)*(f(k,r)))%mod;
}
return dp[l][r];
}
int main(){
n=read();
for(int i=1;i<=n;++i){
b[i]=read();
}
printf("%lld\n",f(1,n));
return 0;
}
整体来讲,本题要用到的就是巧妙地定义,首先不会有重复,其次是合法性得到了保障。综上,区间 dp 中划分的定义如果有类似的限制,也需要选择两个区间一单一多来保证。
CF721C Journey
相比之下还是简单的。
既然是 DAG 上的背包问题,\(T\) 又非常大,不能作为状态,那么以距离为状态求最大节点数和以节点数为状态求最小距离其实是一样的,处理完之后倒序找距离满足条件的最大节点数即可。所以要用拓扑排序,保证遍历到的不会指向前面的点,这样就近似于一个“线性”的东西,状态 \(dp(i,j)\) 设置成 \(1\) 到 \(i\) 节点路径经过 \(j\) 个节点的最小距离。
最后一个优化:题面要求 \(1\) 到 \(n\) 的距离,于是拓扑序在 \(1\) 前的没啥用,直接跳过。
点击查看代码
int n,m,k;
int dp[5005][5005],pre[5005][5005];
struct Graph{
struct edge{
int to,nxt,w;
}e[5005];
int head[maxn],cnt;
int deg[maxn];
vector<int> V;
inline void add_edge(int u,int v,int w){
e[++cnt].to=v;
e[cnt].w=w;
e[cnt].nxt=head[u];
head[u]=cnt;
++deg[v];
}
inline void toposort(){
queue<int> q;
for(int i=1;i<=n;++i) if(!deg[i]) q.push(i);
while(!q.empty()){
int u=q.front();
q.pop();
V.push_back(u);
for(int j=head[u];j;j=e[j].nxt){
int v=e[j].to;
--deg[v];
if(!deg[v]) q.push(v);
}
}
}
inline void print(int u,int x){
if(x==1){
printf("1 ");
return;
}
print(pre[u][x],x-1);
printf("%d ",u);
}
}G;
int ans;
int main(){
n=read(),m=read(),k=read();
for(int i=1;i<=m;++i){
int u=read(),v=read(),w=read();
G.add_edge(u,v,w);
}
G.toposort();
memset(dp,0x3f,sizeof(dp));
memset(pre,-1,sizeof(pre));
dp[1][1]=0;
bool pd=0;
for(int u:G.V){
if(u==1) pd=1;
else if(!pd) continue;
for(int i=G.head[u];i;i=G.e[i].nxt){
int v=G.e[i].to,w=G.e[i].w;
for(int j=2;j<=n;++j){
//1->u + u->v = 1->v
if(dp[u][j-1]+w<dp[v][j]){
dp[v][j]=dp[u][j-1]+w;
pre[v][j]=u;
}
}
}
}
for(int i=n;i>=2;--i){
if(dp[n][i]<=k){
ans=i;
break;
}
}
printf("%d\n",ans);
G.print(n,ans);
printf("\n");
return 0;
}
CF8C Looking for Order
见 \(n\) 知状压,比较朴素,预处理出距离然后 dp。
有学习意义的一点是,对于这种先后顺序没有影响的方案(也就是 \(\{1,3\}\) 和 \(\{2,4\}\) 先进行哪个都一样),不妨钦定先进行编号较小的一组,也就是当状态为 \((0000)_2\) 时,刷表只进行 \(\{1,2\},\{1,3\},\{1,4\}\) 的,其余重复的方案不再进行。同理,对于 \((000000)_2\),也只进行 \(\{1,2\},\{1,3\},\{1,4\},\{1,5\},\{1,6\}\),接着再钦定 \(2\) 或 \(3\) 为第二组的必定元素,可以做到优化。
点击查看代码
int main(){
sx=read(),sy=read();
n=read();
for(int i=1;i<=n;++i) a[i].x=read(),a[i].y=read();
for(int i=1;i<=n;++i){
d[0][i]=(sx-a[i].x)*(sx-a[i].x)+(sy-a[i].y)*(sy-a[i].y);
for(int j=i+1;j<=n;++j){
d[i][j]=(a[i].x-a[j].x)*(a[i].x-a[j].x)+(a[i].y-a[j].y)*(a[i].y-a[j].y);
}
}
memset(dp,0x3f,sizeof(dp));
dp[0]=0,pre[0]=0;
for(int s=0;s<(1<<n);++s){
if(dp[s]==0x3f3f3f3f) continue;
for(int i=1;i<=n;++i){
if(1<<(i-1)&s) continue;
for(int j=i;j<=n;++j){
if(1<<(j-1)&s) continue;
if(dp[s|(1<<(i-1))|(1<<(j-1))]>dp[s]+d[0][i]+d[0][j]+d[i][j]){
dp[s|(1<<(i-1))|(1<<(j-1))]=dp[s]+d[0][i]+d[0][j]+d[i][j];
pre[s|(1<<(i-1))|(1<<(j-1))]=s;
}
}
break;
}
}
int st=(1<<n)-1;
printf("%d\n",dp[st]);
while(st){
printf("0 ");
int ans=st^pre[st];
for(int i=1;i<=n;++i){
if(ans&(1<<(i-1))) printf("%d ",i);
}
st=pre[st];
}
printf("0\n");
return 0;
}
数据结构
CF19D Points
首先将 \(x\) 离散化。
于是根据 \(x\) 建线段树存 \(y\) 的信息,选取一个“有代表性”的信息存储,考虑到要查询的是“右上方的点中最靠近左下方的坐标”,要保证上方存在,于是存储最大的 \(y\)。
整理一下操作,需要插入、删除以及查询最大值(用来替换线段树的信息),丢到 set 里完美解决。
插入删除非常好做,需要判断一下删除后是否为空。
查询操作限定查询区域 \([x+1,n]\),先左后右去找对应子树内最大值是否存在大于 \(y\) 的,找到满足条件最小的 \(x\),再回到 set 里去找最小的 \(y\),二分即可。
点击查看代码
int n;
vector<int> V;
set<int> S[maxn];
struct Question{
int opt,x,y;
}q[maxn];
struct SegmentTree{
int val[maxm];
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
inline void build(int rt,int l,int r){
val[rt]=-1;
if(l==r) return;
build(lson),build(rson);
}
inline void update(int rt,int l,int r,int p,int k,bool pd){
if(l==r){
if(!pd) val[rt]=k;
else val[rt]=max(val[rt],k);
return;
}
if(p<=mid) update(lson,p,k,pd);
else update(rson,p,k,pd);
val[rt]=max(val[rt<<1],val[rt<<1|1]);
}
inline int query(int rt,int l,int r,int p,int k){
if(l==r){
if(val[rt]>k) return l;
else return -1;
}
int res=-1;
if(p<=mid&&val[rt<<1]>k) res=query(lson,p,k);
if(res==-1&&val[rt<<1|1]>k) res=query(rson,p,k);
return res;
}
}T;
int main(){
n=read();
char s[10];
for(int i=1;i<=n;++i){
scanf("%s",s);
q[i].x=read(),q[i].y=read();
if(s[0]=='a') q[i].opt=1;
else if(s[0]=='r') q[i].opt=2;
else q[i].opt=3;
V.push_back(q[i].x);
}
sort(V.begin(),V.end());
V.erase(unique(V.begin(),V.end()),V.end());
for(int i=1;i<=n;++i) q[i].x=lower_bound(V.begin(),V.end(),q[i].x)-V.begin()+1;
T.build(1,1,V.size());
for(int i=1;i<=n;++i){
if(q[i].opt==1){
S[q[i].x].insert(q[i].y);
T.update(1,1,V.size(),q[i].x,q[i].y,1);
}
else if(q[i].opt==2){
set<int>::iterator it=S[q[i].x].find(q[i].y);
S[q[i].x].erase(it);
if(S[q[i].x].empty()) T.update(1,1,V.size(),q[i].x,-1,0);
else T.update(1,1,V.size(),q[i].x,*S[q[i].x].rbegin(),0);
}
else{
int res;
if(q[i].x!=V.size()) res=T.query(1,1,V.size(),q[i].x+1,q[i].y);
else res=-1;
if(res==-1) printf("-1\n");
else{
set<int>::iterator it=S[res].upper_bound(q[i].y);
printf("%d %d\n",V[res-1],*it);
}
}
}
return 0;
}
然后这题启发我们一个东西。一般处理数据的方法是让扫过的区间内都符合某种前提条件再加以判断,也就是说我们需要奇妙的把数据排序,但面对这种二维的,似乎符合条件的区域(即选择点的右上角部分)没办法做到连续,于是不如直接横纵坐标拆开处理。
CF343D Water Tree
树链剖分,貌似没有什么可说的。
点击查看代码
int n,m;
struct SegmentTree{
int val[maxn<<2];
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
inline void build(int rt,int l,int r){
if(l==r){
val[rt]=0;
return;
}
val[rt]=-1;
build(lson),build(rson);
}
inline void push_down(int rt,int l,int r){
if(val[rt]!=-1&&l!=r){
val[rt<<1]=val[rt<<1|1]=val[rt];
val[rt]=-1;
}
}
inline void update(int rt,int l,int r,int pl,int pr,int k){
if(pl<=l&&r<=pr){
val[rt]=k;
return;
}
push_down(rt,l,r);
if(pl<=mid) update(lson,pl,pr,k);
if(pr>mid) update(rson,pl,pr,k);
}
inline int query(int rt,int l,int r,int p){
if(l==r){
return val[rt];
}
push_down(rt,l,r);
if(p<=mid) return query(lson,p);
else return query(rson,p);
}
#undef mid
#undef lson
#undef rson
}T;
struct Graph{
struct edge{
int to,nxt;
}e[maxn<<1];
int cnt,head[maxn];
inline void add_edge(int u,int v){
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
int fa[maxn],son[maxn],dep[maxn],siz[maxn];
int dfn[maxn],top[maxn],dfncnt;
inline void dfs1(int u,int f,int d){
fa[u]=f,dep[u]=d,siz[u]=1;
int maxson=-1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>maxson){
maxson=siz[v],son[u]=v;
}
}
}
inline void dfs2(int u,int t){
dfn[u]=++dfncnt,top[u]=t;
if(!son[u]) return;
dfs2(son[u],t);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
inline void update_tree(int x){
T.update(1,1,n,dfn[x],dfn[x]+siz[x]-1,1);
}
inline void update_path(int x){
while(top[x]!=1){
T.update(1,1,n,dfn[top[x]],dfn[x],0);
x=fa[top[x]];
}
T.update(1,1,n,1,dfn[x],0);
}
inline int query(int x){
return T.query(1,1,n,dfn[x]);
}
}G;
CF909D Colorful Points
这是数据结构?
发现这种操作是一个两败俱伤的事情,也就是说一个字母相同的连续区间,与相邻区间的边界会被干掉,并且除了最左和最右的区间以外每次会干掉左右共两个元素。这样的话我们可以维护每个区间内的颜色以及大小,每次干掉区间再合并可以合并的,直到只有一个或没有区间。
时间复杂度是这样的,每次循环时干掉元素一共进行区间个数次,同时最多合并区间个数个的区间,而最多能干掉 \(n\) 个元素,复杂度 \(O(n)\)
点击查看代码
int n;
char s[maxn];
struct node{
int col,siz;
}a[maxn];
int cnt,ans;
int main(){
scanf("%s",s+1);
n=strlen(s+1);
a[++cnt].col=s[1],a[cnt].siz=1;
for(int i=2;i<=n;++i){
if(s[i]==s[i-1]) ++a[cnt].siz;
else a[++cnt].col=s[i],a[cnt].siz=1;
}
while(1){
if(cnt<=1) break;
++ans;
for(int i=1;i<=cnt;++i){
if(i==1||i==cnt) --a[i].siz;
else a[i].siz-=2;
}
int j=0;
for(int i=1;i<=cnt;++i){
if(a[i].siz<=0) continue;
if(a[i].col==a[j].col) a[j].siz+=a[i].siz;
else a[++j]=a[i];
}
cnt=j;
}
printf("%d\n",ans);
return 0;
}
数学
图论
字符串
杂项
标签:return,乱写,res,CF,int,区间,inline,杂题,dp 来源: https://www.cnblogs.com/SoyTony/p/15875357.html