杂题总结 Vol.1

杂题总结 Vol.1

Status: CLOSED

记号约定

不难注意

意味着我在初见的时候想到了的部分

难以注意

意味着我没有注意到的部分

\(\def\EZ{\textcolor{#51af44}{\text{EZ}}}\EZ\) 表示简单,10分钟内就能想到。
\(\def\HD{\textcolor{#3173b3}{\text{HD}}}\HD\) 表示中等,能独立想出
\(\def\IN{\textcolor{#be2d23}{\text{IN}}}\IN\) 表示困难,独立思考能想到 \(50\%\) 以上
\(\def\AT{\textcolor{#383838}{\text{AT}}}\AT\) 表示非常困难,独立思考只能想出 \(50\%\) 以下

P8421 [THUPC2022 决赛] rsraogps

\(\IN\)
P8421 [THUPC2022 决赛] rsraogps - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

首先套路性扫描线,然后这个问题就变成增量构造。

不难注意

不知怎么的,就注意到当扫描到 \(x\),往前暴力更新的时候过了一个界限就不会再更新了,这是因为这三个运算的性质,越与二进制中 1 的位置越少,越或二进制中 0 的位置越少,越 gcd 因数就越少,所以暴力枚举这个界限的时间复杂度好像有一个上界。

没错,就是 \(\mathcal O(\log v)\) 的。考虑什么时候停止枚举,对于与和或,当不再变化的时候停止,每枚举一次 0/1 的数量都会下降,至多枚举 \(\mathcal O(\log v)\) 次。而对于 \(\gcd\),当不再变化的时候停止吗,最坏情况下每一个数都是后一个数的倍数,此时由于值域限制,也是至多 \(\mathcal O(\log v)\) ,即每一个数是后一个数的 \(2\) 倍。

那么找到界限之后,前面的部分就不变,怎么维护?

实际上,界限之前的内容就只会变化上一次变化的内容。

难以注意

怎么维护呢?

我们可以为每一个位置记录上一次变化的多少(再为这个做前缀和),然后打上时间戳。

P5292 [HNOI2019] 校园旅行

\(\AT\)
[HNOI2019] 校园旅行 - 洛谷

难以注意

其实这题应该注意到朴素 dp 做法。因为我们要求的是回文串相关的信息,我们就考虑构造回文串可以用中心扩展法,那么如果已经有一条路径,从两头继续扩展也该是合法路径。这时我们发现这好像就是一个转移。验证一下,由于重复走环没有意义,所以这种做法是正确的。


哎呀,然后你就发现 30pts 到手了,那么这不得不令人怀疑 100pts 的做法是不是优化这个。

那么我们考虑如果一条合法路径的两头 \(x,y\),分类讨论:

  • \(x\) 旁边有 \(\texttt{0-1}\) 边,那么通过走若干次,可以扩展 \(\texttt{1-0-1-0}\) 这样

    如果决定不回来,就能 \(\texttt{1-0-1}\) 这样

  • \(x\) 旁边有 \(\texttt{0-0}\) 边,那么可以走若干次, \(\texttt{0-0-0-0}\) (偶数长度)

那么我们现在发现,来回走 这个操作在一定程度上可以充当走环的作用,那么如果可以用 来回走 来代替环的功效,就不需要再保存环了!

那么究竟可不可以代替呢?

就比如如果 \(x,y\) 旁边都有 \(\texttt{0}\) 连通块,原本如果都想去到连通块内的一个点 \(x',y'\) 还要保持合法,那么也许在连通块里面绕了若干次,如果这个若干次我们可以通过来回走搞定,那么不就好了吗?

但是来回走因为要回到原地,所以只能处理中间绕了偶数下的,如果连通块中间没有长度为奇数的环,那么这样就可以替代,由于两边可以多绕几圈来同步,所以替代之后能不能匹配是不需要担心的,那么我们只需要保留连通性,那么保留生成树。

如果有只因环,那么只有这一条边来回走就不够了,可以额外连接一个自环来允许改变奇偶性。

对于 \(\texttt{0-1}\) 的连通块也是这样。所以千万别再理解成相同颜色连通块缩点再保留生成树了,是在只有异色边的子图上操作。

我们可以发现,不含有奇环的图,就是二分图,染色判断即可。

省略:[SCOI2009] 粉刷匠 (luogu.com.cn) 原因:\(\EZ\)

P5290 [十二省联考 2019] 春节十二响

\(\HD\)
[十二省联考 2019] 春节十二响 (luogu.com.cn)

不难注意

我们可以考虑链的情况,显然就是把两边最大的合并到最大的,然后一路合并下去。

那么这个可不可以推广呢,不知怎么的就可以注意到,他是可以的,简单思考一下,如果已经得到一个子树的最优分组,那么肯定不能再次合并,所以两边是一对一合并的关系,因为两棵子树之间没有任何其他限制,感性理解满足最优子结构性质。利用启发式合并可以做。

P5840 [COCI2015] Divljak

\(\HD\)
[COCI2015] Divljak (luogu.com.cn)

有点难以注意

考虑对于 \(S\) 建立 AC 自动机,那么添加 \(T\),在 AC 自动机上面跑的时候,我们发现如果是求出现次数,那么就是经典问题,求子树和即可,但是这样会重复。那么我们不可以直接在 fail 树上加到根,我们还得减去重复部分,像虚树一样,如果把所有点按照 fail 上的 dfn 排序,那么重复部分在 fail 树上就是一条到根的路径,这样就好处理了,每次询问统计子树里面的修改标记即可。

但是我想了个根号分治,思考流程在下面:

试试根号分治 
当 n 大的时候,那么 每一个字符串长度小设为 l 
字符串 hash 开 map 总共  O(len*l) 的
此时 ---- O(len*l) 
当 n 小的时候,那么 字符串每一个长
可执行暴力跳 fail(记录上面上一个end在哪) 的做法每次 O(n) 
此时 ---- O(qn) 
这样阈值取 B=len/sqrt(q) 为 6324 是最优的
大概 6e8 的样子 

当然,hash 表常数过大,过不了。可是实际上只做当 n 小的算法也可以通过。

P3215 [HNOI2011] 括号修复 / [JSOI2011] 括号序列

[HNOI2011] 括号修复 / [JSOI2011] 括号序列 (luogu.com.cn)
\(\IN\)

难以注意

首先区间翻转明示我们要用平衡树。

应该先模拟一下,能够发现最后一定剩下若干右括号后面跟着若干左括号。根据经典套路,令 \((=-1,)=1\),可以知道剩下右括号的数量就是前缀最大值,剩下左括号的数量就是后缀最小值(允许为空)。用平衡树可以简单维护。

P3153 [CQOI2009] 跳舞

\(\EZ\)

不难注意

比较简单一眼,就是提一下可以在残量网络上加边继续做这样的一个算法。

P4336 [SHOI2016] 黑暗前的幻想乡

\(\HD\)

主要靠这个再理解一下容斥

这个先咕一会

P5445 [APIO2019] 路灯

\(\IN\)

其实本题就一个核心 idea 差分

不难注意

一个灯点亮的时候会把其左边右边最长能够扩展到的询问给点亮。一个灯熄灭的时候会把左边右边能扩展到的询问都给熄灭。

难以注意

怎么™维护这个呢,我们注意到每次影响的询问都是一段,这些询问亮起熄灭的时间也是一段一段的,当他们被影响的时候就会结束或者开启一段。这种问题,我们如果维护被点亮的询问集合一一处理,时间复杂度太高,如果用差分的思想在首尾处理就很好,所以注意到每一段对于一个询问的贡献是 \(-st+ed\),故使用二维线段树,批量将一些询问 \(-st\) 或者 \(+ed\)

P3979 遥远的国度

\(\EZ\)

不难注意

这种东西肯定是树链剖分,然后这个换根肯定没法进行,考虑做分类讨论,重新计算答案

Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,INF=INT_MAX;
int a[N],n,m;
vector<int> e[N];
struct TCSInfo{
    int hson,top,dep,dfn,fa,siz;
}s[N];
int idx[N],tim,mark[N];
void dfs1(int x,int fa){
    s[x].fa=fa,s[x].siz=1,
    s[x].dep=s[fa].dep+1,s[x].hson=0;
    for(int v:e[x]){
        if(v==fa) continue;
        dfs1(v,x),s[x].siz+=s[v].siz;
        if(s[x].siz>s[s[x].hson].siz)
            s[x].hson=v;
    }
}
void dfs2(int x,int top){
    s[x].top=top,s[x].dfn=++tim,idx[tim]=x;
    if(s[x].hson) dfs2(s[x].hson,top);
    for(int v:e[x]){
        if(v==s[x].fa or v==s[x].hson)
            continue;
        dfs2(v,v);
    }
}
void TCSInit(){dfs1(1,0),dfs2(1,1);}
struct SegT{
    int l,r;
    int cov,edt,min,flg;
}t[N*4];
#define l(p) (t[p].l)
#define r(p) (t[p].r)
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define cov(p) (t[p].cov)
#define edt(p) (t[p].edt)
#define minv(p) (t[p].min)
#define flg(p) (t[p].flg)
void Update(int p){minv(p)=min(minv(ls(p)),minv(rs(p)));}
void Build(int p,int l,int r){
    l(p)=l,r(p)=r,edt(p)=-1;
    if(l==r) return minv(p)=a[idx[l]],void();
    int mid=(l+r)/2;
    Build(ls(p),l,mid),Build(rs(p),mid+1,r);
    Update(p);
}
void AlterCov(int p,int v){cov(p)=v,minv(p)=v;}
void AlterEdt(int p,int v){edt(p)=v,flg(p)=v;}
void Spread(int p){
    if(cov(p))
        AlterCov(ls(p),cov(p)),
        AlterCov(rs(p),cov(p)),cov(p)=0;
    if(edt(p)!=-1)
        AlterEdt(ls(p),edt(p)),
        AlterEdt(rs(p),edt(p)),edt(p)=-1;
}
void CModify(int p,int l,int r,int v){
    if(l<=l(p) and r(p)<=r) return AlterCov(p,v);
    int mid=(l(p)+r(p))/2;Spread(p);
    if(l<=mid) CModify(ls(p),l,r,v);
    if(r>mid)  CModify(rs(p),l,r,v);
    Update(p);
}
void FModify(int p,int l,int r,int f){
    if(l<=l(p) and r(p)<=r) return AlterEdt(p,f);
    int mid=(l(p)+r(p))/2;Spread(p);
    if(l<=mid) FModify(ls(p),l,r,f);
    if(r>mid)  FModify(rs(p),l,r,f);
    Update(p);
}
int Query(int p,int l,int r){
    if(r<l) return INF;
    if(l<=l(p) and r(p)<=r) return minv(p);
    int mid=(l(p)+r(p))/2;Spread(p);
    if(r<=mid) return Query(ls(p),l,r);
    if(l>mid)  return Query(rs(p),l,r);
    return min(Query(ls(p),l,r),Query(rs(p),l,r));
}
int IsEdt(int p,int x){
    if(l(p)==r(p)) return flg(p);
    int mid=(l(p)+r(p))/2;Spread(p);
    if(x<=mid) return IsEdt(ls(p),x);
    else       return IsEdt(rs(p),x);
}
//TCS Oper
void FChain(int x,int v){
    while(x){
        FModify(1,s[s[x].top].dfn,s[x].dfn,v);
        mark[s[s[x].top].fa]=(v==1?s[x].top:0);
        x=s[s[x].top].fa;
    }
}
int root=1;
void ChangeRoot(int x){
    FChain(root,0),FChain(x,1),root=x;
}
int QueryExcept(int x,int y){
    return min(Query(1,s[x].dfn,s[y].dfn-1),
    Query(1,s[y].dfn+s[y].siz,s[x].dfn+s[x].siz-1));
}
int QueryForEdt(int x){
//    cerr<<"LLL\n";
    if(x==root) return Query(1,1,n);
    int exp=(mark[x]?mark[x]:s[x].hson);
    return min(QueryExcept(x,exp),QueryExcept(1,x));
}
int QueryForNormal(int x){
//    cerr<<"LPN\n";
    return Query(1,s[x].dfn,s[x].dfn+s[x].siz-1);
}
int Ask(int x){
    if(IsEdt(1,s[x].dfn)) return QueryForEdt(x);
    else                    return QueryForNormal(x);
}
void ChangeChain(int x,int y,int v){
    while(s[x].top!=s[y].top){
        if(s[s[x].top].dep<s[s[y].top].dep)
            swap(x,y);
        CModify(1,s[s[x].top].dfn,s[x].dfn,v);
        x=s[s[x].top].fa;
    }
    if(s[x].dep>s[y].dep) swap(x,y);
    CModify(1,s[x].dfn,s[y].dfn,v);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<n;i++){
        int u,v;cin>>u>>v;
        e[u].emplace_back(v),
        e[v].emplace_back(u);
    }
    for(int i=1;i<=n;i++) cin>>a[i];
    TCSInit(),Build(1,1,n);int id;
    cin>>id,ChangeRoot(id);
    while(m--){
        int op,x;cin>>op>>x;
        if(op==1) ChangeRoot(x);
        else if(op==3) cout<<Ask(x)<<"\n";
        else{
            int y,v;cin>>y>>v;
            ChangeChain(x,y,v);
        }
    }
    return 0;
}
/*FOOTNOTE
排除某一个子树的答案其实是可以快速做的
改变根这个操作实际上只有在从新根到原来的根
路径上的点会排除儿子的答案,加上原来的根排除自己的答案
树链剖分的时候额外打一个标记即可,换根的时候要清除原来的标记
,跳轻边的时候记得记录下儿子是从哪个子树里来的 
*/

P3225 [HNOI2012] 矿场搭建

\(\HD\)

作者考场猜天真结论被制裁了((

不难注意

首先天真的结论就是:每个点双都放一个

不是,哥们,你有没有想过万一塌的是那个点呢?

你可能会回答“那就朝其他点双跑啊”,对啊,那就朝其他点双跑啊,那么为什么你个小可爱要建这么多点呢?

你可能会回答“那如果堵住了呢”,好问题。

现在考虑什么情况下不能向外跑,显然是点双之内有一/零个割点的情况,前者意味着那个割点坍塌之后就没有通往点双外的路径,后者意味着图是点双连通的,也没有其他地方可去。

那么这些地方必须放置,其他地方总是能够通向这些放了的位置。

等一下,你都不质疑一下我吗? 万一只有一个“一割点”的点双,而那个割点被堵住了呢?
这显然是不可能的。原图缩点之后构成一棵树,“一割点”的点双本质上就是这棵树的"一度点"。 如果只有一个叶子,那么必然意味着根也是“一割点”的点双。 万一那个点塌了怎么办?
往另一个“一割点”点双逃啊。 那么如果是双连通的呢?
恭喜你,发现了特殊情况,这个时候需要建立 2 个出口。

P4563 [JXOI2018] 守卫

\(\IN\)

不难注意

最简单,却最有用的结论:一个区间的右端点必选。

难以注意

决策问题里面遇到有什么必做决策的时候,注意观察去掉这个必做决策的状态之后的子问题特性,毕竟这种问题通常使用 DP 解决。

本题,去掉所有被右端点监控到的节点之后,就会发现剩下了一些区间。观察他们之间互相的影响是什么,我们发现,这些区间肯定是要比那些去掉的点低的,否则他们就可以被右端点监视到,所以两个区间之间是不能互相监视的。这就很重要了,这说明区间之间是独立的,我们可以采用区间 dp。

至于优化,应该比较容易,难点全在于如何意识到这是区间 dp,就这一点,甚至值得评紫。

P3688 [ZJOI2017] 树状数组

P3688 [ZJOI2017] 树状数组 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

\(\HD\)

独立想出了,结果没注意到直接线段树维护概率是不对的,因为其实对于一次操作涉及的所有位置来说,这次操作对其的影响不是独立的,之间维护会导致一次操作可以选出多个位置。

不难注意

并不是很难地能够发现,可怜写了一个能够求后缀和的树状数组。

当然不发现这个也没关系(比如我就没发现这个),我们只需要查看这种操作对查询的影响即可。容易发现询问就是异或操作,所以只看为 \(1\) 的点,一个点 \(x\) 如果为 \(1\) 对询问 \([l,r]\) 的影响:

  • \(x\notin [l-1,r]\) 没有影响

  • \(x\in[l,r-1]\) 也没有影响

  • \(x=l-1\) 的时候,会导致多异或 \(1\)

  • \(x=r\) 的时候会少异或 \(1\)

这样我们就发现,如果 3、4 同时出现,其实答案也是对的,但是当 3、4 不是同时出现,那么就会出错,所以答案就是,\(\mathbf P(a_{l-1}=a_r)\)

由于开头我们所说的那个问题,我们很难直接对序列进行操作,但是我们发现操作对于询问的影响是独立的,那么可以二维线段树维护询问,每次进行操作即可。我们在线段树上维护某一个 \((l,r)\) 不相等的概率,每次乘上一个改变的概率,容易用矩阵维护,这个矩阵是一个对称矩阵,很特别,具有乘法交换律,我们可以采用标记永久化的方法维护,也很容易把矩阵简化掉。

P5336 [THUSC2016] 成绩单

\(\HD\)

独立做出一个区间 dp 的紫题,应该纪念一下。

不难注意

不难注意到应该将状态设为 \(f_{l,r,a,b}\) 表示区间 \([l,r]\) 中包含 \(l\) 那一次选择中的最大为 \(a\) 最小为 \(b\) 且使得 \([l,r]\) 花费最小的方案的花费值。

事实上没有必要限制是不是包含 \(l\),更优秀的方案是设 \(f_{l,r}\) 为发完的最小花费,\(g_{l,r,a,b}\) 是发 \([l,r]\) 之后还剩下的那些中间最大值为 \(a\) 最小值为 \(b\) 的。这样其实更自然一些(实际上我最开始设计 \(f\) 数组的初衷与设计 \(g\) 数组是一样的,但是实现的时候实现坏了,就改了定义)

Code
#include<bits/stdc++.h>
using namespace std;
const int N=55;
int n,_a,_b,w[N],f[N][N][N][N],tmp[N][N];
int main(){
    cin>>n>>_a>>_b;
    for(int i=1;i<=n;i++) cin>>w[i];
    memset(f,0x3f,sizeof f);
    for(int i=1;i<=n;i++) f[i][i][i][i]=_a;
    for(int len=2;len<=n;len++){
        for(int l=1;l<=n;l++){
            int r=l+len-1;
            if(r>n) break;
            int mi=INT_MAX,miid=0;
            int mx=INT_MIN,mxid=0;
            for(int k=l;k<r;k++){
                if(w[k]<mi) mi=w[k],miid=k;
                if(w[k]>mx) mx=w[k],mxid=k;
                for(int g=k+1;g<=r;g++){
                    int lpres=INT_MAX;
                    for(int a=k+1;a<g;a++)
                        for(int b=k+1;b<g;b++)
                            lpres=min(lpres,f[k+1][g-1][a][b]);
                    if(k+1==g) lpres=0;
                    for(int a=l;a<=r;a++)
                        for(int b=l;b<=r;b++)
                            tmp[a][b]=0x3f3f3f3f;
                    for(int a=g;a<=r;a++){
                        for(int b=g;b<=r;b++){
                            int mmi=min(mi,w[b]);
                            int mmx=max(mx,w[a]);
                            int iid=(mi<w[b]?miid:b),xid=(mx>w[a]?mxid:a);
                            tmp[xid][iid]=min(tmp[xid][iid],
                                lpres+f[g][r][a][b]-_b*(w[a]-w[b])*(w[a]-w[b])+
                                                _b*(mmx-mmi)*(mmx-mmi));
                            tmp[mxid][miid]=min(tmp[mxid][miid],lpres+f[g][r][a][b]+_a+_b*(mx-mi)*(mx-mi));
                            tmp[a][b]=min(tmp[a][b],lpres+f[g][r][a][b]+_a+_b*(mx-mi)*(mx-mi));
                        }
                    }
                    for(int a=l;a<=r;a++)
                        for(int b=l;b<=r;b++)
                            f[l][r][a][b]=min(f[l][r][a][b],tmp[a][b]);    
                }
            }
        }
    }
    int ans=INT_MAX;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
         ans=min(ans,f[1][n][i][j]);
    cout<<ans;
}
/*FOOTNOTE
TMD 我是不是搞错了 怎么数据这么小
我知道了 他是拿出来一组然后两边合起来
那么区间 dp 我们枚举区间中的子区间即可 
这样做 n^4 预处理 n^3 
预计 BLUE 
不行 我应该再记录一个“区间中最后取的一段的max/min是哪一个数”
f[l][r][a][b] 这样做利用刷表法
枚举外区间 n^2 枚举最后取的一段的第一段 n
枚举后面取得 min/max n^2
最后复杂度 n^5 1/2 
n+(n-1)*2+(n-2)*3+(n-3)*4
ok 的 常数非常小 
也是够唐的 
*/

P6088 [JSOI2015] 字符串树

\(\EZ\)

太简单了不记

P6835 [Cnoi2020] 线形生物

\(\EZ\)

推动下面这个式子即可,其中 \(deg_x\) 代表 \(x\) 有多少返祖边,\(sl_x\) 表示自环数量,\(sum_x=\sum_{i=1}^x g_x\)\(g_x\) 代表到达 \(x\) 之后期望需要多少步数才能移动到 \(x+1\)

\[\begin{aligned} g_x=&\sum_{i=1}^{\infty}\left(\frac{deg_x}{deg_x+1}\right)^{i-1} \left(\frac{1}{deg_x+1}+ \frac{sl_x}{deg_x+1}+ \sum_j{\frac{1}{deg_x}\left(sum_{x-1}-sum_{j-1}+1\right)}\right)\\ =&1+sl_x+\sum_j{\left(sum_{x-1}-sum_{j-1}+1\right)} \end{aligned} \]

答案是 \(sum_{n+1}\)

P2447 [SDOI2019] 外星千足虫

\(\EZ\)

是简单的线性代数应用。记得消元成最简形的时候不要像我这个唐诗学习,只消上一个。

P2481 [SDOI2010] 代码拍卖会

\(\IN\)

容易注意

难搞的时候,通常是还有性质没有发现。数位 dp 常常是构造性的,尝试考虑构造性的思路,一个合法方案的差分数组是比较简单的形式,即若干个和在 \([1,9]\) 的数 。这是从序列的角度考虑。

难以注意

变换角度,从数值的方向考虑,构造这个差分数组相当于枚举断点,相当于在差分数组某个位置加上 \(1\),等价于数值上加上一段 \(1\)。那么我们就将问题转化为若干( \(\mathcal O(1)\) 个)\(111111\) 相加。

实际上,如果直接做,那么跟构造差分数组没什么区别。这就是很多人容易忽略这个思路的原因。但是考虑到 \(P\le 500\) 则数值有一个优点就是很多数值实质上是模 \(P\) 等价的。这样就可以简化问题了。直接跑背包即可。

P2485 [SDOI2011] 计算器

\(\EZ\)

不是真的有这种模板三合一。。。

只是对 BSGS 记录一点易错:

  1. 如果转化为 \(A^{am+b}\) 就要考虑逆元的问题,但是 \(A^{am-b}\) 就不用考虑。

  2. 当没有模数比两个操作数大这个条件的时候注意特判 \(A,B\equiv 0\) 的情况且做之前先取模。

P5385 [Cnoi2019] 须臾幻境

\(\IN\)

我们在考虑单次询问的时候,常常会使用并查集解决这个问题,我们要考虑扩展这个做法。

如果能够离线,那么我们可能会考虑维护动态的并查集并使用莫队,这显然可以用 LCT 实现。并查集做法是等价于维护一个生成树的,每加上一条树边,答案就减去1,这个启发我们转化答案为范围内有多少边可以作为树边。如果我们把离线处理的生成树保存,就能处理在线询问。

所以我们应该考虑在线转离线也可以采用扫描线预处理的方式。

生成树其实是有很多个的,但是我们扫描的时候总应该优先使用靠近 \(r\) 的边构造生成树,考虑一个询问其实就是询问 \((l,r)\) 在这种最优的生成树上存在多少边。保存这种结果,可以使用主席树。

P9755 [CSP-S 2023] 种树

\(\EZ\)

其实是执念吧,去年做不出来,现在终于来弥补遗憾了。


给了答案的范围,真是耐人寻味,二分答案。

可以计算出一棵树要在 mid 之前长大的最晚种植时间,这个是个单调函数,也可以二分。

我们只要按照这个时间贪心地去种植模拟,暴力跳fa即可 。

P3965 [TJOI2013] 循环格

\(\HD\)

今天做的时候脑子里一直在播放奇怪的歌曲,有点头昏脑胀,不大清醒,这么简单个结论竟然没看出来,堪称唐诗。


首先感觉 DP 是非常难以处理这个问题的,考虑用图来表达一下这个关系。我们发现原图是基环内向树,我们的目标是让其变成若干环。

非常经典的技巧就是,可以从图的度数方面考虑,显然最后的图满足 入度=出度=1

然后要注意一下,感觉上这个东西可以用网络流。每个点都可以选择把自己的 1 出度贡献给四个相邻点的 1 入度,每个点要且只能被贡献 1 入度。这样拆点就可以了,根据最大流的最大性,显然满足所有点都被贡献了 1 入度(因为一定是有解的)。

P5904 [POI2014] HOT-Hotels 加强版

\(\IN\)

容易有这样的想法:找到中心点,以他为根,从三个不同子树中选择三个距离相等的点。

求取这个东西,很容易想到用线段树合并。但是线段树合并只能处理子树内部的,难以处理子树之外的情况,但是子树之外只能选择一个,有两个都在子树之内。

考虑换个角度看待询问,看成在两个子树里面选择两个再在外面选择一个。在两个子树里面选择两个仍然需要线段树合并。

考虑线段树合并一次在叶子节点上的意义,就是选择被合并子树中的一个点与前面那些子树中的一个点组成一对,然后还需要第三个点,考虑总体减去部分得到另外的点数,即求离某个点距离为 \(x\) 的点数,考虑要这么求的次数并不多,是和线段树合并复杂度同阶的,把这些询问离线下来之后就是经典问题,可以用点分治解决。

P3736 [HAOI2016] 字符合并

\(\HD\)

容易注意

我们会观察到若干次操作总是包含或者不交的,这是区间 dp 的重要特征。

由于所有分数都是正数,所以每个区间最优的答案一定要合成到不能再合成,这个最后的长度可以直接计算。

考虑这样的性质,可以简化枚举:每种方案总是有一种划分满足要么是两边直接拼接要么是只使用一次全局的合并。

难以注意

那么特判全局合并之后,让右区间只合并成最后一个数即可避免枚举剩余,这个性质也肯定了主动转移法的正确性,从这个角度思考更容易得到最后这一步,也更容易实现。

posted @ 2024-09-04 10:14  haozexu  阅读(43)  评论(0)    收藏  举报