P11842 [USACO25FEB] Bessie's Function G
SmokingTurtle · · 题解
前置芝士
基环树和基环树森林
如果一张无向连通图包含恰好一个环,则称它是一棵基环树。
如果一张有向连通图每个点的入度都为 1,则称它是一棵基环外向树。
如果一张有向连通图每个点的出度都为 1,则称它是一棵基环内向树。
多棵基环树可以组成基环森林,多棵基环外向树可以组成基环外向树森林,多棵基环内向树可以组成基环内向森林。
——摘自 OIWIKI
基环树一般的处理办法转化为树上问题+环上问题。具体来说,一般会将环断开,变为若干棵树,每棵树单独计算,最后在环上统计答案。
tortoise-hare(乌龟-野兔)算法
见我的这篇专栏
题意概括
给定数列
题目分析
建模
我们发现题目中用到了
考虑一个非常典的模型:将数列下标视为点,然后从
此时,由于有
显然,连通块之间互不影响,所以我们单独考虑每一个基环树。
我们发现,如果要修改
对于一种将
a_i 修改为x\ \left(x\neq i\right) 的方案,我们知道此时不存在满足a_j=i 的j (不然a_j=i\text{ 而 }a_{a_j}=x )。既然如此,我们只需要考虑i 和a_i 的代价。此时,如果
a_i\neq i ,那么要求a_{a_i}=a_i 。然而,如果将a_i 改为i ,则不会增加代价,而且不会出现这条附加限制,所以肯定不劣。
接下来,我们考虑不修改的
于是,现在问题变成了:在基环树上选择若干个点,使得如果
我们考虑转化为树上问题+环上问题(前文所说的套路)。
求解
找环
这里可以用前文所说的 tortoise-hare 算法。
树上部分
对于树上部分,他是一个比较典的树形 DP 题目。在没有上司的舞会这道题中将最大化收益改为最小化代价即可。具体的,令
大概思路是令
转移方程:
upd 2025/03/23:第二个方程之前打错了,感谢 @lcy6
原因读者可以自行推导或者看上面提到的没有上司的舞会的题解,这里不再赘述。
环上问题
不妨设这个环上的所有点依次为
环上问题一般有三种解法:
- 将环复制两遍,转化为在一个长度为
2\times len 的序列上面寻找一个最优的长度为len 的区间。这个是最基础的,但是一般较慢(因为区间有O(n^2) 个) - 随意找一个地方将环断开,然后枚举最后一项和第一项之间的关系,转化为两个相似的序列问题。例题:P6064 Naptime。
- 设每个点的答案分别为
dp_1, dp_2,\cdots, dp_n ,求出相互之间关系,高斯消元求解。这种方法一般适用于期望 DP,较为复杂,超出了本题的讨论范围。
这里,我们肯定是选用第二种方法。具体的,设
那么转移方程和
其实维护两个变量即可,但是感觉这样稍微清晰一点。
那么初始值和最终答案是多少呢?那我们枚举
-
> 此时,$ring_0$ 必须修改,所以 $f_{0,0}=dp_{ring_0,0},\ f_{0,1}=+\infin$。由于我们钦定 $ring_{len-1}$ 的 $a$ 不修改,所以最终答案为 $f_{len-1,1}$。 -
> 此时,$ring_0$ 可以修改也可以不修改,所以 $f_{0,0}=dp_{ring_0,0},\ f_{0,1}=dp_{ring_0,1}$。由于我们钦定 $ring_{len-1}$ 的 $a$ 修改,所以最终答案为 $f_{len-1,0}$。
因此,对于这两种情况,我们各自计算一次
总结
现在,我们已经做完了这道题,再简要总结一下方法:
- 首先,我们找到所有连通块。
- 其次,对于每个连通块,我们找到环。
- 然后,我们断开环上的边,对于环上每个点都作为根跑一遍树形 DP。
- 接着,在环上再跑两次 DP 统计答案。
-
最后,把所有连通块的答案全部加起来得到最终答案。
code
#include<iostream> #include<vector> using namespace std; long long dp[200005][2],f[200005][2],c[200005]; bool done[200005],onring[200005]; vector<int> out[200005],ring; const long long inf=4e18; int in[200005]; void dfs(int u)//求 dp 数组的值 { done[u]=1; dp[u][0]=(u!=in[u])*c[u]; for(int v:out[u]) if(!onring[v] && !done[v]) dfs(v),dp[u][0]+=min(dp[v][0],dp[v][1]),dp[u][1]+=dp[v][0]; } long long deal(int x)//处理包含 x 的连通块的答案 { ring.clear(); int y=x; do ring.push_back(y),onring[y]=1,y=in[y]; while(y!=x);//找环 for(int u:ring) dfs(u);//计算dp int len=ring.size(); if(len==1) return min(dp[ring[0]][0],dp[ring[0]][1]); for(int i=0;i<len;i++) f[i][0]=f[i][1]=inf; f[0][0]=dp[ring[0]][0]; for(int i=1;i<len;i++) f[i][0]=min(f[i][0],min(f[i-1][0],f[i-1][1])+dp[ring[i]][0]), f[i][1]=min(f[i][1],f[i-1][0]+dp[ring[i]][1]); long long ans=min(f[len-1][0],f[len-1][1]); for(int i=0;i<len;i++) f[i][0]=f[i][1]=inf; f[0][0]=dp[ring[0]][0]; f[0][1]=dp[ring[0]][1]; for(int i=1;i<len;i++) f[i][0]=min(f[i][0],min(f[i-1][0],f[i-1][1])+dp[ring[i]][0]), f[i][1]=min(f[i][1],f[i-1][0]+dp[ring[i]][1]); ans=min(ans,f[len-1][0]); return ans; } int main() { int n; cin>>n; long long ans=0; for(int i=1;i<=n;i++) cin>>in[i],out[in[i]].push_back(i); for(int i=1;i<=n;i++) cin>>c[i]; for(int i=1;i<=n;i++) if(!done[i])//如果i所在连通块没有被遍历 { int x=in[i],y=i; while(x!=y) x=in[in[x]],y=in[y];//Floyd's Tortoise and Hare algorithm 算法 long long tmp=deal(x); ans+=tmp;//最终对所有连通块求和 } cout<<ans<<endl; }