Atcoder Grand Contest 007&008
Shik and Travel
题目描述
解法
首先上来二分答案 \(k\),然后变成判定性问题。
在树上走可以很容易联系到 \(dp\),发现我们要记录的信息是走到子树内第一个叶子的距离 \(a\),和从子树内最后一个叶子走回来的距离 \(b\),这样转移的时候就可以把左右子树拼起来。
设 \(f(u,a)\) 表示子树 \(u\) 内,\(a\) 对应最小的 \(b\) 是多少。转移时可以看成左右儿子有若干个二元组 \((a,b)\),设 \(x,y\) 为边权,我们这样考虑合并 \((a_1,b_1)\) 和 \((a_2,b_2)\)(要做两次,可以左一右二,也可以左二右一):
- 如果 \(x+b_1+y_1+a_2\leq k\),那么会产生二元组 \((a_1+x,b_2+y)\)
考虑扫描所有 \((a_1,b_1)\),那么我们取尽量大的 \(a_2\),就可以得到尽量小的 \(b_2+y\),显然这是一个双指针问题。
这样做的复杂度是多少呢?考虑状态数较少的那么为 \(sz\),那么它作为 \(1\) 的时候会产生恰好 \(sz\) 个状态,它作为 \(2\) 的时候只有 \(sz\) 个状态是会被保留的(\(a\leq a'\and b\leq b'\) 会排除掉 \((a',b')\)),所以在 \(u\) 上产生了 \(2\cdot sz\) 个状态。并且我们可以如果一个状态被扫描过一次就会被抛弃,所以复杂度就是新增状态总数,可以得到复杂度是 \(O(n\log n)\) 的。
总时间复杂度 \(O(n\log n\log a)\),我的代码中偷懒用了排序所以多了一个 \(\log\)
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 200005;
#define int long long
#define pii pair<int,int>
#define pb push_back
#define x first
#define y second
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,k,ans,ch[M][2],c[M][2];vector<pii> v[M];
void dfs(int u)
{
v[u].clear();
if(!ch[u][0]) {v[u].pb({0,0});return ;}
dfs(ch[u][0]);dfs(ch[u][1]);
vector<pii> vc;
for(int d=0;d<2;d++)
{
int ls=ch[u][d],rs=ch[u][d^1];
int lc=c[u][d],rc=c[u][d^1],tmp=k-lc-rc;
for(int i=0,j=0;i<v[ls].size();i++)
{
while(j+1<v[rs].size() && v[rs][j+1].x+v[ls][i].y<=tmp)
j++;
if(j>=v[rs].size() || v[rs][j].x+v[ls][i].y>tmp) continue;
vc.pb({v[ls][i].x+lc,v[rs][j].y+rc});
}
}
sort(vc.begin(),vc.end());
for(auto t:vc)
{
if(!v[u].empty() && v[u].back().y<=t.y) continue;
v[u].pb(t);
}
}
signed main()
{
n=read();
for(int i=2;i<=n;i++)
{
int j=read(),w=ch[j][0]>0;
ch[j][w]=i;c[j][w]=read();
}
int l=0,r=1e12;
while(l<=r)
{
k=(l+r)>>1;dfs(1);
if(v[1].empty()) l=k+1;
else r=k-1,ans=k;
}
printf("%lld\n",ans);
}
Next or Nextnext
题目描述
解法
做这道题真的太难受了,理解起来困难,代码细节写错,不知道花了多少时间\(...\)
首先把题目限制做一个转化,我们使用经典建图 \(i\rightarrow p_i\),那么建出来一定是若干个环,并且每个点在环上跳一步或者跳两步可以到达 \(a_i\),我们称之为答案图。
为了更充分的思考 \(a_i\) 的限制我们建出限制图,也就是 \(i\rightarrow a_i\) 的图,我们综合思考这两个图。考虑限制图是一棵基环内向树,但是如果我们从答案图的角度去构建限制图,每条边在环上的距离都 \(\leq 2\),所以只有这些情况:
- 如果答案图的长度是偶数,那么限制图可以原封不动,或者分裂成两个长度相等的环(都跳两步)
- 如果答案图的长度是奇数,那么限制图可以原封不动,或者变成另一个环(都跳两步,非自环)
- 有可能把限制图构建成无分岔的基环内向树(因为只能跳一步或者两步,所以一旦有分叉环就接不上)
因为现在我们只知道限制图,所以我们通过上述情况还原答案图来计数:
- 对于不存在支链的环:可以把两个长度相同的环拼起来;可以单独还原一个环,如果环是奇数且非自环,则有单独弄有两种情况;否则只能原封不动。这里可以通过简单 \(dp\) 实现(决策是拼起来还是自己搞)
- 对于存在支链的环,可以看下图:
设 \(lspace\) 表示上一个支链的根到这个支链的根的距离(特别地,如果只有一个支链那么 \(lspace=n\)),\(ltree\) 表示这个支链的长度,两种情况分别是在根前面放支链的节点或者是放环上的节点,那么方案数是 \([lspace\geq ltree]+[lspace>ltree]\),判断方案可不可行的方法就是看够不够把支链塞到环里面。
总结
双图策略可以帮助你充分考虑已知信息和所求,通过思考图的关系来发现问题性质。
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,tot,ans,f[M],a[M],b[M],c[M],vis[M],dep[M];
struct edge{int v,next;}e[M];
void dfs(int u)
{
dep[u]=1;int ls=0;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(vis[v]==2) continue;
if(ls) {puts("0");exit(0);}
ls=1;dfs(v);dep[u]=dep[v]+1;
}
}
signed main()
{
n=read();ans=1;
for(int i=1;i<=n;i++)
{
a[i]=read();//i->a[i]
e[++tot]=edge{i,f[a[i]]},f[a[i]]=tot;//a[i]->i
}
for(int i=1;i<=n;i++) if(!dep[i])
{
int t=0,x=i,lst=1;
for(;!vis[x];x=a[x]) vis[x]=1;
for(;vis[x]==1;x=a[x]) vis[x]=2,c[++t]=x;
for(int j=t;j>=1;j--)
{
dfs(c[j]);
if(lst==1 && dep[c[j]]!=1) lst=j-t;
}
if(lst==1) {b[t]++;continue;}
for(int j=1;j<=t;j++) if(dep[c[j]]>1)
{
int ls=j-lst,lt=dep[c[j]]-1;
if(ls<lt) {puts("0");exit(0);}
if(ls>lt) ans=ans*2%MOD;
lst=j;
}
}
for(int i=1;i<=n;i++) if(b[i])
{
int lst=0,now=1,nxt=0;
for(int j=1;j<=b[i];j++)
{
nxt=((i&1)&&i!=1)?(now<<1):now;
nxt=(nxt+(j-1)*lst%MOD*i)%MOD;
lst=now;now=nxt;
}
ans=ans*now%MOD;
}
printf("%lld\n",ans);
}
Black Radius
题目描述
解法
还是要多动笔,这样题解才看懂得快,效率高 (☞゚ヮ゚)☞
设 \(f(x,d)\) 表示以 \(x\) 为圆心,\(d\) 为半径的点集。首先我们不考虑全部点都被染黑的情况,那么对于一个点集,得到它并且使得半径 \(d\) 最小的点 \(u\) 是固定的。这说明如果有多个 \(f(x,d)\) 表示的点集相同,我们在使得 \(d\) 最小的 \(x\) 处统计一次贡献就不会算重。
考虑简化问题:所有点都可以作为起点。那么我们考虑 \(d\) 最小的要求是:
- 不能覆盖完所有点,设 \(mx[x]\) 为从 \(x\) 开始的最长链,则:\(d\leq mx[x]-1\)
- 对于任意邻接点 \(y\),需要满足:\(f(x,d)\not=f(y,d-1)\),设 \(se[x]\) 表示不考虑 \(y\) 方向的最长链,那么:\(d-2<se[x]\),因为如果 \(d-2\geq se[x]\) 那么 \(y\) 可以把 \(x\) 方向的点覆盖完,而它们在 \(y\) 方向的覆盖范围相同,所以这样整体覆盖范围就相同了,不合法。
回到本题,我们把可以作为起点的点称为关键点。我们考虑把非关键点的贡献迁移到关键点上,也就是如果 \(f(x,d_1)\) 可以让 \(d\) 最小,但是 \(x\) 不是关键点,如果可以找到 \(f(y,d_2)=f(x,d_1)\) 并且 \(y\) 是关键点,那么它还是可以产生贡献。
结论:若 \(f(x,d_1)\) 使得 \(d_1\) 最小并且可以找到 \(f(x,d_1)=f(y,d_2)\),等价于 \(f(x,d_1)\) 覆盖完 \(y\) 方向的子树(注意这里并不是 \(y\) 的子树,而是连在 \(x\) 上的对应方向的子树)
首先证明充分性,设 \(v\) 是 \(y\) 方向上的第一个点,因为 \(f(x,d_1)\not=f(v,d_1-1)\),而因为非 \(y\) 方向节点它们的覆盖范围相同,所以 \(f(y,d_2)\) 从 \(v\) 的角度看还剩下的覆盖范围是 \(\geq d_1\) 的,它的覆盖范围大于 \(f(x,d_1)\) 的 \(d_1-1\),而它们在 \(y\) 方向的覆盖方向相同,所以它们一定覆盖完了 \(y\) 方向的子树。
必要性也是容易证明的,显然这样的 \(d_2\) 是可以找到的。
应用上述结论,对于非关键点的贡献,\(d\) 的下界应该是存在关键点子树的最长链的最小值,所以 \(d\) 的合法取值范围一定是区间,我们用换根 \(dp\) 可以轻易地求出它。
//You may miss me deep down
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
const int inf = 0x3f3f3f3f;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,tot,f[M],mx[M],se[M],d[M],sz[M];char s[M];
struct edge{int v,next;}e[M<<1];long long ans;
void dfs1(int u,int fa)
{
if(s[u]=='1') d[u]=0,sz[u]=1;
else d[u]=inf;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa) continue;
dfs1(v,u);sz[u]+=sz[v];
if(mx[v]+1>mx[u]) se[u]=mx[u],mx[u]=mx[v]+1;
else if(mx[v]+1>se[u]) se[u]=mx[v]+1;
if(sz[v]) d[u]=min(d[u],mx[v]+1);
}
}
void dfs2(int u,int fa)
{
int up=min(se[u]+1,mx[u]-1);
if(d[u]<=up) ans+=up-d[u]+1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa) continue;
int t=(mx[v]+1==mx[u])?se[u]:mx[u];t++;
if(t>mx[v]) se[v]=mx[v],mx[v]=t;
else if(t>se[v]) se[v]=t;
if(sz[1]-sz[v]) d[v]=min(d[v],t);
dfs2(v,u);
}
}
signed main()
{
n=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
e[++tot]=edge{v,f[u]},f[u]=tot;
e[++tot]=edge{u,f[v]},f[v]=tot;
}
scanf("%s",s+1);
dfs1(1,0);dfs2(1,0);
printf("%lld\n",ans+1);
}