vp + 补题 + 随机做题 记录二
按随机顺序排序
[IOI2024] Sphinx
0.前言
我在从零开始做这道题的时候,做到链部分分时突然发现能做正解,然后实现了一下这个依赖随机化的做法,然而最终还是没能卡过去,无奈学习了询问次数确定的正解。
本文侧重于做这道题的思路过程以及两种做法的比较。
1.团
交互题经常使用二分解决问题,毕竟交互函数往往是可以同时处理多个子问题的,我们有若干备选项肯定希望一次能淘汰更多的,然而我们能做的只有把子问题划分出若干个集合依次判断,那么往往会想到的就是各分一半做。
那么在团里面,一个非常好的性质是邻域足够大,也就是邻域大小等于总颜色数量(可以给当前点预留一个颜色,如果剩下 \(n-1\) 个颜色都不是,那么肯定是预留的颜色)。
于是就可以直接二分,每次确定某个点的颜色在哪个子区间内,判断把邻域染色就可以了。
询问次数大概是 \(O(n\lceil\log n\rceil)\) 的。
2.链
到这里难度骤升,至少我这么认为。
团的做法不能拓展到此处,毕竟邻域最大只有 2。
我个人在做的时候,首先想的是可以类似线段树一样,外层枚举颜色,内层查询这个颜色的儿子数量,做到询问次数均摊 \(O(n\lceil\log n\rceil)\) 。
那么问题就在于怎么查询区间内某个颜色的点的数量,很显然的一个方法是分奇偶性做两遍,判断时把奇数染 -1 偶数染 \(c\)(第二遍反之)。
我实现了一下,效果很差,随机数据都要 4000 次左右,原因在于有很多无效判断(区间内没有这个颜色),无效判断无益于减少势能。
然后我想,可以提前求出来这个颜色有多少个,找到所有的后就直接停掉。
那么怎么求呢?
如果还按奇偶性分组去做,那么出现一个这个颜色的点,同色联通块减少可能是 1/2 中任意一个,这显然不能求。
那么按模 3 的值分组呢?
这时就可行了,只需要三个一组,组之间用一个 \(n\) 当一堵墙,这样彼此之间互不干扰了。
至于末尾不成三个一组的,两个一组不影响,单个一组就需要特判了,至少我当时没什么好想法。
这样确实好了很多,询问次数可以到 3200 次左右,但是仍然太多。
关键在于求数量的时候还需要额外付出 3 次查询,还能少一些吗?
当然!我们继续考虑按模 4 的值分组。
这个时候,我们可以先染 -1 c -1 n,再染 n -1 c -1,只需要两次查询就可以查到颜色数量了……吗?
我们还需要讨论末尾那些剩余的。
三个一组没影响,两个一组特殊一点,需要把第二遍改成 c -1 n -1,不过也问题不大。单个一组怎么搞?
我一开始实现了暴力搞单个那组,线段树那部分改成了 4 叉树(不知道为什么的确比 2 叉树询问次数少一些),结果居然真的拿到了 64 分。
但是自己造的随机的 \(n=4k+1\) 的数据就会卡到 3000 次。IOI 数据太水。
不过加上随机打乱后就能跑到限制以内了。
3.好像能拓展?
我们现在回顾一下上述链做法依赖什么:
- 待求点集是个独立集
好像只依赖这个?
那么我们不妨把图拆成若干个独立集,每个独立集跑链的做法。
如果只能找到单点的独立集,那么剩下的是一个团,尽管这个团可能不大,但我们改造一下二分的过程,单次二分询问次数变为 \(O(\lfloor\log n\rfloor+\lceil\frac{n}{|adj_x|}\rceil)\) ,其中 \(adj_x\) 是 \(x\) 的邻域,粗略分析应该还是能做到 \(O(n\lceil\log n\rceil)\)
那么我们就希望把图划分成尽量少的独立集。
我先实现了不考虑尽量少划分这一条件的版本,效果很差,甚至随机数据都要 8000 次左右,不过起码这样做正确性没问题。
那么怎么划分会好一些呢?
我先想到的是把图中度数比较大的点去掉,这样可能会好一些,去掉这个点就使用类似团做法的二分即可。
实现以后发现可以做到随机数据 2500 次,调一调参数可以过很多点。
但这样远远不够,我构造了一棵基环树,发现询问次数会达到 20000+ 次!
那么说明不应该删那么多点,只需要达到一定的稀疏程度即可,于是我让存在的边数小于等于存在的点数的常数倍后停止闪点转做独立集。
我个人能力有限,这个随机化的做法我认为止步于此了。
现在来谈一谈正解。
4.询问次数上限确定的解法
点开题解区我才发现有部分分……
题解区链做法个人认为大同小异,对应着我对于我的链做法依赖条件的疏忽:
- 只需要选出的点集就算有边也不影响染色判断,也就是点集内的边一定连接颜色不同的点。
这也就是部分分存在的意义吧,大概。
那么我们缩完同色连通块后,把生成树黑白染色,任意拿出来一个点集,无疑都满足了这个依赖条件。
于是这个题就做完了,询问次数上界分析 虎神讲的非常明白。
Bonus 1
官方数据最多询问次数只有 2400 左右。(虎神说有构造上界 2745 的数据,不太懂)。
那是否意味着随机化能卡过去?
Bonus 2
如果只求每种颜色有多少个点,那么能做到多少询问次数呢?(链的上界至少是 \(3n\) )。
代码
#include "sphinx.h"
#include <vector>
#include <queue>
#include <cstdio>
#include <cassert>
#include <algorithm>
#include <cstring>
int perform_experiment(std::vector<int> E);
using namespace std;
#define ep emplace_back
int n;
int mp[255][255], mp2[255][255], bel[255];
vector<int> C;
vector<int> vec[255]; int idx;
int fa[255];
int get(int x){
if(x==fa[x]) return x;
return fa[x]=get(fa[x]);
}
bool merge(int x, int y){
x=get(x); y=get(y);
if(x==y) return false;
fa[y]=x; return true;
}
int EXP(const std::vector<int>& S) {
int ret=0;
for(int i=0; i<n; ++i) fa[i]=i, ret+=(S[i]==n);
for(int i=0; i<n; ++i) if(S[i]==n){
for(int j=0; j<n; ++j) if(S[j]==n&&mp[i][j]){
ret-=merge(i, j);
}
}
return ret;
}
int EXP2(const std::vector<int>& S, int c) {
int components_cnt = 0;
std::vector<bool> vis(n, false);
for (int i = 0; i < n; i++) if(S[i]==n||S[i]==c){
if (vis[i]) continue;
components_cnt++;
std::queue<int> q;
vis[i] = true;
q.push(i);
while (!q.empty()) {
int cur = q.front();
q.pop();
for (int nxt=0; nxt<n; ++nxt) if((S[nxt]==n||S[nxt]==c)&&mp[cur][nxt]==1){
if (!vis[nxt] && S[nxt] == S[cur]) {
vis[nxt] = true;
q.push(nxt);
}
}
}
}
return components_cnt;
}
bool vis[255];
vector<int> v[2];
void dfs(int x, int o){
v[o].ep(x); vis[x]=1;
for(int i=0; i<idx; ++i) if(!vis[i]&&mp2[x][i]) dfs(i, o^1);
}
bool qry(int l, int r, int c){
if(l>r) return false;
vector<int> E;
for(int i=0; i<n; ++i) E.ep(c);
for(int i=l; i<=r; ++i) for(auto t:vec[v[0][i]]) E[t]=-1;
return EXP2(E, c)+(r-l+1)!=perform_experiment(E);
}
std::vector<int> find_colours(int N, std::vector<int> fx, std::vector<int> fy) {
n=N;
for(int i=0; i<n; ++i) C.ep(-1);
for(int i=0; i<(int)fx.size(); ++i) mp[fx[i]][fy[i]]=mp[fy[i]][fx[i]]=1;
vec[0].ep(0); bel[0]=0; ++idx;
for(int i=1; i<n; ++i){
vec[idx].ep(i); bel[i]=idx; ++idx;
vector<int> E;
for(int j=0; j<n; ++j) E.ep(n);
for(int j=0; j<idx; ++j){
for(auto t:vec[j]) E[t]=-1;
}
int v1=EXP(E)+idx, v2=perform_experiment(E);
while(v1!=v2){
int l=1, r=idx-2, mid, ret=0;
while(l<=r){
mid=(l+r)>>1;
for(int j=0; j<n; ++j) E[j]=n;
for(int j=0; j<idx; ++j){
for(auto t:vec[j]) E[t]=(j>=l&&j<=mid)?-1:n;
}
for(auto t:vec[idx-1]) E[t]=-1;
if(EXP(E)+(mid-l+1)+1!=perform_experiment(E)) {
ret=mid; r=mid-1;
}
else l=mid+1;
}
for(auto t:vec[bel[i]]) vec[ret].ep(t), bel[t]=ret;
vec[idx-1].clear();
--idx;
vec[idx-1].swap(vec[bel[i]]);
int mem=bel[i];
for(auto t:vec[mem]) bel[t]=mem;
for(auto t:vec[idx-1]) bel[t]=idx-1;
for(int j=0; j<n; ++j) E[j]=n;
for(int j=0; j<idx; ++j){
for(auto t:vec[j]) E[t]=-1;
}
if(idx==1) break;
v1=EXP(E)+idx, v2=perform_experiment(E);
}
}
if(idx==1){
vector<int> E;
for(int i=0; i<n; ++i) E.ep(n);
E[0]=-1; int link=-1;
for(int j=0; j<n; ++j) if(mp[0][j]) link=j;
for(int i=0; i<n; ++i) {
E[link]=i;
int cur=perform_experiment(E);
if(cur==1+EXP(E)) {
for(int j=0; j<n; ++j) C[j]=i;
break;
}
}
return C;
}
for(int i=0; i<(int)fx.size(); ++i){
if(bel[fx[i]]!=bel[fy[i]]){
mp2[bel[fx[i]]][bel[fy[i]]]=1;
mp2[bel[fy[i]]][bel[fx[i]]]=1;
}
}
dfs(0, 0);
for(int i=0; i<n; ++i) {
while(qry(0, v[0].size()-1, i)){
int l=1, r=v[0].size()-1, mid, ret=0;
while(l<=r){
mid=(l+r)>>1;
if(qry(l, mid, i)){
ret=mid; r=mid-1;
}
else l=mid+1;
}
for(auto t:vec[v[0][ret]]) C[t]=i;
vector<int> tem;
for(auto t:v[0]) if(t!=v[0][ret]) tem.ep(t);
v[0]=tem;
}
}
v[0]=v[1];
for(int i=0; i<n; ++i) {
while(qry(0, v[0].size()-1, i)){
int l=1, r=v[0].size()-1, mid, ret=0;
while(l<=r){
mid=(l+r)>>1;
if(qry(l, mid, i)){
ret=mid; r=mid-1;
}
else l=mid+1;
}
for(auto t:vec[v[0][ret]]) C[t]=i;
vector<int> tem;
for(auto t:v[0]) if(t!=v[0][ret]) tem.ep(t);
v[0]=tem;
}
}
return C;
}
[CF2021E3] Digital Village
感谢 韩hungry 提供的极大帮助。
0.E1
猜个结论,每次贪心找能最小化的点,局部最优解就是最优解!
你别说,还真过了。
(其实应该算 part 1,毕竟是对后面有贡献的 part)。
1.E2
显然这是个瓶颈路问题,我们需要求出来瓶颈路树,而我们知道最小生成树就是一类瓶颈路树,所以求出任意一棵最小生成树。
那么就需要在最小生成树上做最优化问题,然后会发现非常不好做。
原因在哪?在于我们即使做完了子树内的问题,子树外的点仍然可以使用子树内的基站,这就破坏了子树内子问题最优的性质。
再考虑我们是一颗瓶颈路树,所以在求生成树时就可以确定每个用户的最大延迟边。
那么我们就在合并过程中做最优化问题,首先想到的是用 DP。
那么我们记 \(f_{G, i}\) 表示在导出子图 \(G\) 中选 \(i\) 个基站的最小值,转移是一个 \((\min, +)\) 卷积,特殊处理一下 \(f_{G,0}\)。
2.E3
你说这都已经 \((\min, +)\) 卷积了,还能没性质?
考虑 E1 我们的贪心,这说明了 \(f_{G}\) 的差分序列是一个单减序列,也就是说 \(f_{G}\) 是凸的。
那么直接用闵可夫斯基和合并即可,用 std::multiset 维护差分,合并时用启发式合并,即可做到 \(O(nlog^2n)\) 的时间复杂度。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=2e5+5;
const ll inf=1e18;
int Test, n, m, p;
struct edg{
int x, y, v;
inline bool operator <(const edg &t)const{
return v<t.v;
}
}E[N];
int s[N];
multiset<ll> f[N];
int val[N];
int fa[N], sz[N], ct[N];
inline int get(int x){
if(x==fa[x]) return x;
return fa[x]=get(fa[x]);
}
inline void merge(int x, int y, int v){
int fx=get(x), fy=get(y);
if(fx==fy) return ;
if(ct[fx]<ct[fy]){
swap(fx, fy); swap(x, y);
}
ll f0=*f[fx].begin(); f[fx].erase(f[fx].begin());
f[fx].insert(f0-(ll)(v-val[fx])*sz[fx]);
f0=*f[fy].begin(); f[fy].erase(f[fy].begin());
f[fy].insert(f0-(ll)(v-val[fy])*sz[fy]);
for(auto t:f[fy]) f[fx].insert(t);
f[fy].clear();
ct[fx]+=ct[fy]; sz[fx]+=sz[fy];
fa[fy]=fx; val[fx]=v;
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(Test);
while(Test--){
read(n); read(m); read(p);
for(int i=1; i<=n; ++i) {
val[i]=0; f[i].clear();
sz[i]=0, f[i].insert(0), ct[i]=1;
}
for(int i=1; i<=p; ++i) read(s[i]), sz[s[i]]=1;
for(int i=1; i<=m; ++i) read(E[i].x), read(E[i].y), read(E[i].v);
sort(E+1, E+m+1);
for(int i=1; i<=n; ++i) fa[i]=i;
for(int i=1; i<=m; ++i) merge(E[i].x, E[i].y, E[i].v);
ll cur=(ll)p*val[get(1)];
for(auto t:f[get(1)]) cur+=t, printf("%lld ", cur);
putchar('\n');
}
return 0;
}
[CF1817F] Entangled Substrings
标签:SAM,基本子串结构
还在役的时候就收藏了的题,可惜 NOI2023 前都没能抽时间写一下。
这个题堪称基本子串结构的第二道模板题了。
0.刚看到题
从零开始审视这道题,我们尝试用基本子串结构与题目要求的子串对的关联。
因为要求这一对子串比如时时刻刻间隔相同的串,所以不难想到两个串必须在同一等价类中,那么只需要求每个等价类内部不交的子串对数。
1.怎么建基本子串结构?
我个人即将完成文章 《浅谈基本子串结构》,也可以学习 xtq 的国家集训队论文或其他博主的文章。
假定我们能使用 \(O(n)\sim O(nlogn)\) 的时间内搭建显示基本子串结构。
2.怎么统计答案?
我们不妨对后半个子串统计等价类里有多少个能成为前半个。
因为我们搭建了显示基本子串结构,所以我们可以线性遍历阶梯形轮廓,那么这变成了一个区间加区间查问题,不过好在可以离线,利用(差分)前缀和就可以统计了。
我不会 \(O(n)\) 搭建的方法,所以我的实现使用的是倍增定位子串的 \(O(nlogn)\) 的搭建方法,不过跑起来还算比较快。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const ll inf=1e18;
const int N=2e5+6;
int Test, n;
char s[N];
struct SAM{
struct node{
int len, c[26], fa;
}sam[N<<1];
int lst, cnt;
int occ[N<<1], id[N<<1], mnpos[N<<1], sz[N<<1];
inline void extend(int i){
int p=lst, np=lst=++cnt; int x=s[i]-'a';
occ[np]=1; pos[i]=np; mnpos[np]=i;
sam[np].len=sam[p].len+1;
for(; p&&!sam[p].c[x]; p=sam[p].fa) sam[p].c[x]=np;
if(!p) sam[np].fa=1;
else{
int q=sam[p].c[x];
if(sam[q].len==sam[p].len+1) sam[np].fa=q;
else{
int nq=++cnt;
sam[nq]=sam[q];
sam[nq].len=sam[p].len+1;
sam[q].fa=sam[np].fa=nq;
for(; p&&sam[p].c[x]==q; p=sam[p].fa) sam[p].c[x]=nq;
}
}
}
vector<int> e[N<<1];
int pos[N];
int f[N<<1][21];
inline void chkmin(int &x, int y){
if(!x) x=y;
else if(y) x=min(x, y);
}
inline void dfs(int x){
for(auto y:e[x]){
f[y][0]=x;
for(int i=1; i<21; ++i) f[y][i]=f[f[y][i-1]][i-1];
dfs(y);
occ[x]+=occ[y]; chkmin(mnpos[x], mnpos[y]);
}
}
inline void init(){
for(int i=1; i<=cnt; ++i) {
e[i].clear(); occ[i]=0; id[i]=0; mnpos[i]=0;
sam[i].fa=sam[i].len=0;
for(int t=0; t<26; ++t) sam[i].c[t]=0;
}
cnt=lst=1;
for(int i=1; i<=n; ++i) extend(i);
for(int i=2; i<=cnt; ++i) e[sam[i].fa].emplace_back(i), sz[i]=sam[i].len-sam[sam[i].fa].len;
dfs(1);
}
inline int locate(int l, int r){
int x=pos[r];
for(int t=20; t>=0; --t) if(sam[f[x][t]].len>=r-l+1) x=f[x][t];
return x;
}
inline void merge(){
for(int i=cnt; i>=2; --i) if(!id[i]){
for(int t=0; t<26; ++t) if(sam[i].c[t]){
id[i]=id[sam[i].c[t]]; break;
}
}
}
}A;
struct SAM2{
struct node{
int len, c[26], fa;
}sam[N<<1];
int lst, cnt;
int occ[N<<1], id[N<<1], mnpos[N<<1], sz[N<<1];
inline void extend(int i){
int p=lst, np=lst=++cnt; int x=s[i]-'a';
occ[np]=1; pos[i]=np; mnpos[np]=i;
sam[np].len=sam[p].len+1;
for(; p&&!sam[p].c[x]; p=sam[p].fa) sam[p].c[x]=np;
if(!p) sam[np].fa=1;
else{
int q=sam[p].c[x];
if(sam[q].len==sam[p].len+1) sam[np].fa=q;
else{
int nq=++cnt;
sam[nq]=sam[q];
sam[nq].len=sam[p].len+1;
sam[q].fa=sam[np].fa=nq;
for(; p&&sam[p].c[x]==q; p=sam[p].fa) sam[p].c[x]=nq;
}
}
}
vector<int> e[N<<1];
int pos[N];
int f[N<<1][21];
inline void chkmax(int &x, int y){
x=max(x, y);
}
inline void dfs(int x){
for(auto y:e[x]){
f[y][0]=x;
for(int i=1; i<21; ++i) f[y][i]=f[f[y][i-1]][i-1];
dfs(y);
occ[x]+=occ[y]; chkmax(mnpos[x], mnpos[y]);
}
}
inline void init(){
for(int i=1; i<=cnt; ++i) {
e[i].clear(); occ[i]=0; id[i]=0; mnpos[i]=0;
sam[i].fa=sam[i].len=0;
for(int t=0; t<26; ++t) sam[i].c[t]=0;
}
cnt=lst=1;
for(int i=1; i<=n; ++i) extend(i);
for(int i=2; i<=cnt; ++i) e[sam[i].fa].emplace_back(i), sz[i]=sam[i].len-sam[sam[i].fa].len;
dfs(1);
}
inline int locate(int l, int r){
int x=pos[r];
for(int t=20; t>=0; --t) if(sam[f[x][t]].len>=r-l+1) x=f[x][t];
return x;
}
inline void merge(){
for(int i=cnt; i>=2; --i) if(!id[i]){
for(int t=0; t<26; ++t) if(sam[i].c[t]){
id[i]=id[sam[i].c[t]]; break;
}
}
}
}B;
int idx;
int repx[N<<1], repy[N<<1];
vector<int> row[N<<1], col[N<<1];
inline void print(__int128 x){
if(x>=10) print(x/10);
putchar(48^(x%10));
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
scanf("%s", s+1); n=strlen(s+1);
A.init();
reverse(s+1, s+n+1);
B.init();
for(int x=2; x<=A.cnt; ++x){
int r=A.mnpos[x], l=r-A.sam[x].len+1;
int y=B.locate(n-r+1, n-l+1);
if(B.sam[y].len==r-l+1) A.id[x]=B.id[y]=++idx;
}
A.merge(); B.merge();
for(int i=A.cnt; i>=2; --i) row[A.id[i]].emplace_back(i);
for(int i=B.cnt; i>=2; --i) col[B.id[i]].emplace_back(i);
__int128 ans=0;
for(int x=1; x<=idx; ++x){
repy[x]=A.mnpos[row[x][0]]; repx[x]=n-B.mnpos[col[x][0]]+1;
int mny=A.mnpos[row[x].back()];
int len=repy[x]-mny+1;
vector<pii> vec;
for(auto t:row[x]) {
int bdx=A.mnpos[t]-A.sam[A.sam[t].fa].len, bdy=A.mnpos[t];
vec.emplace_back(bdx, bdy);
}
for(auto t:col[x]) {
int bdx=B.mnpos[t]-B.sam[B.sam[t].fa].len, bdy=B.mnpos[t];
bdx=n-bdx+1; bdy=n-bdy+1; swap(bdx, bdy);
vec.emplace_back(bdx, bdy);
}
sort(vec.begin(), vec.end());
vec.erase(unique(vec.begin(), vec.end()), vec.end());
vector<int> sum;
sum.resize(len);
for(int i=0; i<(int)vec.size(); ){
int j=i;
while(j+1<(int)vec.size()&&vec[j+1].fi==vec[i].fi) ++j;
++sum[vec[i].se-mny];
i=j+1;
}
for(int i=1; i<len; ++i) sum[i]+=sum[i-1];
ll lst=0;
for(int i=(int)vec.size()-1; i>=0; ){
int j=i;
while(j-1>=0&&vec[j-1].fi==vec[i].fi) --j;
lst+=repy[x]-vec[j].se+1;
if(vec[i].fi-1-mny>=0&&vec[i].fi-1-mny<len) ans+=(__int128)sum[vec[i].fi-1-mny]*lst;
i=j-1;
}
}
print(ans);
return 0;
}
[ABC376G] Treasure Hunting
标签:贪心,堆
这两个题居然能转化。
0.刚看到题
显然可以转化成求一个排列,使得每个点出现在所有祖先后面,最小化 \(\sum_{i=1}^ni\times a[p[i]]\)。
一开始猜可以根据子树和之类的贪心放点,试了很多并不对。
1.弱化版怎么做
如果没有子树的限制,显然不会产生逆序对。
从局部入手,仅考虑一对邻居 \((x, y)\),含有 0/1 的数量分别为 \(f_{0/1},g_{0/1}\)。
那么先放 \(x\) 更优的条件是 \(f_{1}\times g_{0}<f_{0}\times g_{1}\),也就是 \(\frac{f_{1}}{f_{0}}<\frac{g_1}{g_0}\)。
证明了局部调整最优,那么全局也是符合调整最优的,所以按照 \(\frac{f_1}{f_0}\) 排序贪心合并即可,用一个堆和并查集就可以解决。
2.怎么转化
这是最厉害的地方。
我们试着用逆序对来刻画 \(\sum_{i=1}^ni\times a[p[i]]\),不妨假设每个点有若干个 0/1,我们通过构造让两个问题等价。
如果只有一个点,那么需要产生 \(1\times a[1]\) 个逆序对,我们不妨让 0 号点只有一个 1,让 1 号点有 \(a[1]\) 个 0。
如果只有两个点,那么需要第一个点产生 \(1\times a[p[1]]\) 、第二个点产生 \(2\times a[p[2]]\) 个逆序对。
我们尽量让点之间独立,所以考虑第二个点内部有 \(a[p[2]]\) 个 0,前面有恰好 \(2\) 个 1。0 号点已经有一个 1 了,所以需要让第一个点内部也有一个 1。
因为不能破坏第一个点的 0,所以只能把这个 1 放在那些 0 的末尾。
这样,我们就看出来怎么构造了:每个点是一个数组,由 \(a[i]\) 个 0 和 末尾 \(1\) 个 1 组成。
于是跑一下弱化版的问题即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=2e5+5, mod=998244353;
int Test, n;
int p[N], a[N];
inline ll fpow(ll x, ll y){
ll ret=1;
while(y){
if(y&1) ret=ret*x%mod;
x=x*x%mod;
y>>=1;
}
return ret;
}
struct node{
int c0, c1, id;
friend inline bool operator <(const node &x, const node &y){
return (ll)x.c1*y.c0>(ll)x.c0*y.c1;
}
};
bool vis[N];
int fa[N], cnt0[N], cnt1[N];
inline int gf(int x){
while(x!=fa[x]) x=fa[x]=fa[fa[x]];
return x;
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(Test);
while(Test--){
read(n);
for(int i=1; i<=n; ++i) read(p[i]);
for(int i=0; i<=n; ++i) vis[i]=0, fa[i]=i;
a[n+1]=0;
for(int i=1; i<=n; ++i) read(a[i]), a[n+1]+=a[i], cnt1[i]=1, cnt0[i]=a[i];
cnt1[0]=1;
priority_queue<node> pq;
ll ans=0;
for(int i=0; i<=n; ++i) pq.push((node){a[i], 1, i});
while(pq.size()){
node cur=pq.top(); pq.pop();
if(cur.id!=gf(cur.id)) continue;
vis[cur.id]=1;
if(cur.id==0) continue;
int x=gf(p[cur.id]);
ans+=(ll)cnt1[x]*cnt0[cur.id];
cnt0[x]+=cnt0[cur.id]; cnt1[x]+=cnt1[cur.id];
fa[cur.id]=x;
pq.push((node){cnt0[x], cnt1[x], x});
}
printf("%lld\n", ans%mod*fpow(a[n+1], mod-2)%mod);
}
return 0;
}
[ARC184E] Accumulating Many Times
0.打表找找规律
这样的序列变换肯定是有周期性的,不妨打表看一下变化的规律。
去掉前导零,这些位置不会对后面产生影响。
通过打表不难发现,所有长度在 \((2^{k-1}, 2^k]\) 的序列,周期为 \(2^k\) 。
但是这个结论并不好严谨证明,先放着不证明。
1.怎么求一个 \(f(i,j)\)
可以直接转 \(O(m)\) 圈,做到 \(O(m^2)\) 求一个,这样肯定是不行的。
考虑一下操作的数学意义。操作 \(k\) 次后,对于位置 \(i\),\(a^{k}_{i}\) 是由 \(a_{j<i}\) 乘一个转移系数得到的,这有点类似于从 \(j\) 走到 \(i\),恰好走了 \(k\) 步,实际上路径数就是这个转移系数,这个转移系数可以用插板法或 gf 得到。
简单计算后我们可以得到 \(a^{k}_i=\sum_{i<j}a_jC_{j-i+k-1}^{k-1}\)。
这是一个卷积的形式,但是好像并不能优化计算一个 \(f(i,j)\)。
不过这里面的运算都是在模 2 意义下的,所以不妨考虑 lucas ,变成了 \(a^{k}_i=\sum_{i<j}a_j[k-1\subseteq j-i+k-1]\)。
考虑一段前缀,如果现在的步数满足了前 \(2^{t}-1\) 个位置 \(A_i\) 和 \(A_j\) 相等,我给步数增加高位并不会影响前缀转移系数的值,从而变成了 \(O(logm)\) 个独立的子问题。
于是我们只需要枚举所有的形如 \(2^{t}-1\) 的前缀,那么依次枚举判断要不要新增高位即可。
至此,我们实现了 \(O(mlogm)\) 计算一个 \(f(i,j)\)。
2.还有什么办法求一个 \(f(i,j)\)?
我们显然不能两两枚举单次 \(O(mlogm)\) 求,不过上述做法对后面的内容仍有很大帮助。
上述做法缺点在于非常依赖两个序列的每个值,我们试着减少依赖性。
回想我们打表的结论,每个序列都存在一个 \(O(序列有效长度)\) 的周期,我们会想到把所有串看成点,一步转移看着连边,所有点构成了不交的若干个环。
-
这里证一下周期,证明周期后因为每个点只有唯一出边,所以结构显然是不交的若干个环。
-
设当前序列(有效)长度为 \(n\)。
-
周期不可能不存在,因为一共只有 \(2^{n-1}\) 种可能的序列,经过 \(2^{n-1}\) 次变换后根据鸽巢原理总会出现重复序列。
-
周期最多是 \(2^{\lceil log_2{n}\rceil}\),因为 \(\forall (j-i)<n\leq 2^{\lceil log_2{n}\rceil}\),补一个高位不影响取值。
-
如果将周期变小,假设新周期 \(np\) 的最高位是 \(t<\lceil log_2{n}\rceil\),一定存在一些 \(j\) 和 \(k\) 满足 \([k-1\subseteq j-1+k-1]\neq [k-1+np\subseteq j-1+k-1+np]\),这样第一项的转移系数发生了变化,因为序列的首项一定为 1,所以序列也一定变为不同的情况。
-
综上,序列的周期 \(p=2^{\lceil log_2{n}\rceil}\)
那么两个序列能互相转化当且仅当在同一个环上。
假如我们对每个环钦定一个代表状态,求出来(每个给出的序列状态)按图中转移边到达(所属环的代表状态)的步数 \(b_i\),那么只需要对每个环求 \(\sum_{i<j}(b_i-b_j)\mod length\),\(length\) 表示环长。
这个代表状态只需要满足好算就行了,我们不妨选择字典序最大的状态,那就从小到大贪心。
因为周期的原因,对于非 \(2^t+1\) 形式的位置,它必须先满足最近的 \(2^t+1\) 形式的位置的贪心,而这个最优位置只会在当前周期出现一次,所以非 \(2^t+1\) 形式的位置是不需要处理的。
计算 \(b_i\) 的复杂度是 \(\sum_{i=0}^{\lfloor\log_2{m}\rfloor}2^i=O(m)\),所以这一部分计算是 \(O(nm)\) 的。
3.还剩什么要解决?
还剩下两个问题:归类和求答案。
归类没什么好方法,用卷积或 gf 做就好了,复杂度 \(O(nmlogm)\)。
之后统计答案只需要树状数组维护逆序对即可,复杂度 \(O(nmlogm)\)。
综上,复杂度为 \(O(nmlogm)\),瓶颈在于归类和求答案。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
//略去poly class
const int N=1e6+6;
// const int N=16;
int n, m;
mt19937_64 rnd(time(0));
ull hs[N];
inline ull HS(poly x){
ull res=0;
for(int i=0; i<m; ++i) if(x[i]) res^=hs[i];
return res;
}
unordered_map<ull, poly> h[N];
inline poly calc(poly f, int t){
poly g;
turn(g, m);
for(int i=0; i<m; ++i) g[i]=(t&(t+i))==t;
f=Mul(f, g);
turn(f, m);
for(auto &v:f) v&=1;
return f;
}
inline pair<poly, int> init(){
poly f;
read(f, m);
int it=0;
while(it<m&&(!f[it])) ++it;
int res=0;
for(int j=1; j<=m-it; j<<=1){
int cur=0;
for(int k=it; k<=it+j&&k<m; ++k) cur^=f[k]&((res&(it+j-k+res))==res);
if(!cur) res|=j;
}
return mapa(calc(f, res), res);
}
struct fenwick{
poly tr;
inline void add(int x){
++x;
for(; x<len(tr); x+=(x&-x)) ++tr[x];
}
inline int get(int x){
++x;
int res=0;
for(; x; x-=(x&-x)) res+=tr[x];
return res;
}
};
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(n); read(m);
if(n==1){
printf("0\n");
return 0;
}
for(int i=0; i<m; ++i) hs[i]=rnd();
for(int i=1; i<=n; ++i) {
pair<poly, int> cur=init();
int it=0;
while(it<m&&(!cur.fi[it])) ++it;
if(m-it) h[m-it][HS(cur.fi)].ep(cur.se+1);
}
int ans=0;
int len=1;
for(int i=1; i<=m; ++i) if(!h[i].empty()){
while((len|1)<=i) len<<=1;
for(auto t:h[i]) if(t.se.size()>1){
fenwick fw;
turn(fw.tr, len+2);
int pre0=0, pre1=0;
for(auto tt:t.se){
tt%=len;
ans=plu(ans, sub(pre1, (ll)pre0*tt%mod));
ans=plu(ans, (ll)fw.get(tt-1)*len%mod);
++pre0; pre1=plu(pre1, tt); fw.add(tt);
}
}
}
printf("%d\n", ans);
return 0;
}
[CF2023F] Hills and Pits
0.刚看到题
如果区间和非负肯定有解,并且答案上界为 \(2(r-l)\)。
我一开始的想法是线段树上维护(从左进从右出)、(从右进从左出)、(任意位置进左出)、(任意位置进右出),类似最大子段和合并,但是没办法同时记录最小值和当前沙子的数量,大概是不能做。
1.固定起点为 1 ,终点为 \(n\)
题解第一句话就限制了一个非常强的条件:起点 \(1\) 终点 \(n\)。
原因我认为在于如果存在一种计算固定起点终点的方法,那么就存在单次询问 \(n^2\) 的做法,就也许能上 DS 维护这个函数。那就从起点 \(1\) 终点 \(n\) 这种特殊情况入手。
从前缀和角度考虑,我们会发现如果 \(sum_i\geq 0\),那么 \(i\sim i+1\) 这段路只需要走一次,因为我们肯定可以在走这段路前把 \(1\sim i\) 的子问题全处理好。
再考虑 \(sum_i<0\),那么 \(i\sim i+1\) 就需要至少走 3 次,需要先走到这里、从后面借沙子、回来填上、再向后走。
事实上,这段路只需要走 3 次:遇到 \(sum_i<0\) 时,我们走到后面最近的 \(sum_j\geq 0\) 的位置就回去填坑,这样就能达到下界。
2.固定起点和终点
那么再考虑仅固定起点和终点 \(s\) 和 \(t\),要求 \(s\leq t\)(翻转做两遍)。
我们把路线分三段构造:填好 \(1\sim s-1\),填好 \(s\sim t\),填好 \(t+1\sim n\)。
如果 \(sum_{s-1}\geq 0\),那么就恰好需要 \(2(s-1)\) 的时间把 \(1\sim s-1\) 填好。
之后考虑 \(s\sim t\),这段可以类比上述的过程。
最后考虑 \(t+1\sim n\),如果有解也是恰好用 \(2(n-t)\) 的时间填好。
我们证明一定存在最优解能表达为上述形式:
-
如果从 \(s\) 进入后没有先填前面的坑,之后必须重复走路回来填这段前缀,所以从 \(s\) 进入后最优策略一定是先填前缀,这也要求 \(sum_{s-1}\geq 0\) 这个前置条件。
-
那么就相当于变成了起点固定在 \(1\) 的子问题(从 \(s\) 填完前缀后回到 \(s\),只剩下 \(s\sim n\) 要填,而我们现在处于 \(s\))。我们需要确定终点。
-
最坏的情况肯定是找不到一段后缀可以一来一回填好,那我们就不得不把终点固定到 \(n\)。
-
否则,有这样一个不是 \(n\) 的终点肯定比终点固定在 \(n\) 好,而中间那一段就可以看做起点为 \(1\) 终点为 \(n\) 的问题。
-
综上,存在最优解能表达为上述形式。
(严谨证明大概会很复杂,咕)
那就写一下式子:令 \(f(s,t)\) 表示起点在 \(s\) 终点在 \(t\) 的时间,
其实这个式子就已经可以用 DS 优化了,为了更简便我们希望和 \(s,t\) 相关的项更少,补上 \(2(t-s)\) 后就变成
这是一个最小子段和的形式,用单点修改区间最小子段和 DS 就可以解决本题。
3.细节
如果区间只有一个数还是正数,只需要原地平地,不需要移动,输出 0。
在线做的话大概需要主席树,空间会变成 \(O(nlogn)\),离线的话空间是线性的。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=3e5+5, inf=1e9;
int TEST, n, m;
int a[N], ql[N], qr[N];
int ans[N];
int rk[N];
ll s[N];
inline bool cmp(int x, int y){
return s[ql[x]-1]<s[ql[y]-1];
}
struct info{
int lmn, rmn, mn, sum;
info(int _lmn=0, int _rmn=0, int _mn=0, int _sum=0){lmn=_lmn; rmn=_rmn; mn=_mn; sum=_sum;}
inline void st(int x){
lmn=rmn=mn=sum=x;
}
friend inline info operator +(const info x, const info y){
info ret;
ret.sum=x.sum+y.sum;
ret.lmn=min(x.lmn, x.sum+y.lmn);
ret.rmn=min(y.rmn, y.sum+x.rmn);
ret.mn=min(min(x.mn, y.mn), x.rmn+y.lmn);
return ret;
}
}tr[N<<2];
inline void build(int p, int l, int r){
if(l==r){
tr[p].st(-1);
return ;
}
int mid=(l+r)>>1;
build(p<<1, l, mid); build(p<<1|1, mid+1, r);
tr[p]=tr[p<<1]+tr[p<<1|1];
}
inline void mdf(int p, int l, int r, int x){
if(l==r){
tr[p].st(1);
return ;
}
int mid=(l+r)>>1;
if(x<=mid) mdf(p<<1, l, mid, x);
else mdf(p<<1|1, mid+1, r, x);
tr[p]=tr[p<<1]+tr[p<<1|1];
}
inline void get(int p, int l, int r, int L, int R, info &ret){
if(L<=l&&r<=R){
ret=ret+tr[p];
return ;
}
int mid=(l+r)>>1;
if(L<=mid) get(p<<1, l, mid, L, R, ret);
if(R>mid) get(p<<1|1, mid+1, r, L, R, ret);
}
int rk2[N];
inline bool cmp2(int x, int y){
return s[x]<s[y];
}
inline void solve(){
for(int i=1; i<=n; ++i) s[i]=s[i-1]+a[i], rk2[i]=i;
for(int i=1; i<=m; ++i) rk[i]=i;
sort(rk+1, rk+m+1, cmp);
sort(rk2+1, rk2+n+1, cmp2);
build(1, 1, n);
int it=0;
for(int i=1; i<=m; ++i){
int cid=rk[i];
if(s[qr[cid]]-s[ql[cid]-1]<0) continue;
if(ql[cid]==qr[cid]){
ans[cid]=0; continue;
}
while(it+1<=n&&s[rk2[it+1]]<s[ql[cid]-1]) ++it, mdf(1, 1, n, rk2[it]);
info cur;
get(1, 1, n, ql[cid], qr[cid]-1, cur);
ans[cid]=min(ans[cid], cur.mn+2*(qr[cid]-ql[cid]));
}
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(TEST);
while(TEST--){
read(n); read(m);
for(int i=1; i<=n; ++i) read(a[i]);
for(int i=1; i<=m; ++i) read(ql[i]), read(qr[i]), ans[i]=inf;
solve();
reverse(a+1, a+n+1);
for(int i=1; i<=m; ++i) swap(ql[i], qr[i]), ql[i]=n+1-ql[i], qr[i]=n+1-qr[i];
solve();
for(int i=1; i<=m; ++i) printf("%d\n", ans[i]==inf?-1:ans[i]);
}
return 0;
}
[CF2023E] Tree of Life
0.刚看到题
考虑树形 dp,但是不好设计状态,就先考虑了子树内贪心。
先试了随便找个根,在当前子树内直接把邻居互相连接,邻居的子树直接尽量在当前子树内匹配完成,还有剩余再上传。
样例所有根答案有相同,就只跑以 1 为根,但是 wa。
之后试了一下跑所有根取 min,T,说明有一定正确性。
但匹配这个东西不好用换根 dp 做,就寄了。
1.不能贪吗?
于是这题就是一个反悔贪心。
具体的,我们还是在子树内尽量完成匹配,但是同时记录一下如果需要的话我们能还原出多少个待匹配点。
剩下的就是细节讨论了,这篇文章写的很详细。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=5e5+5;
int Test, n, rt;
vector<int> e[N];
ll ans=0;
int f[N], g[N];
inline void dfs(int x, int fa){
int sz=e[x].size()-(x!=rt);
ans-=(ll)sz*(sz-1)/2;
for(auto y:e[x]) if(y^fa) dfs(y, x);
vector<pii> vec;
for(auto y:e[x]) if(y^fa) vec.ep(f[y], g[y]);
if(x==rt) --sz;
for(auto &t:vec){
if(t.fi>sz){
t.fi-=sz;
}
else{
int cur=sz-t.fi;
if(2*t.se>=cur){
t.se-=cur/2; ans+=cur/2; t.fi=0;
if(cur&1){
t.se--;
++ans;
++t.fi;
}
}
else{
ans+=sz-t.fi-t.se;
t.fi=t.se=0;
}
}
}
f[x]=sz; g[x]=0;
int mx=0, sum=0;
for(auto t:vec){
mx=max(mx, t.fi); sum+=t.fi+2*t.se; ans+=t.se;
}
int mx2=0;
for(auto t:vec) if(mx==t.fi) {sum-=t.fi+2*t.se; mx2=t.se; ans-=mx2; break;}
if(sum>=mx){
f[x]+=(sum+mx)%2;
ans-=(sum+mx)/2;
g[x]+=(sum+mx)/2+mx2;
}
else{
f[x]+=mx-sum;
ans-=sum;
g[x]+=sum+mx2;
}
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(Test);
while(Test--){
read(n);
for(int i=1; i<=n; ++i) e[i].clear();
for(int i=1, x, y; i<n; ++i){
read(x); read(y);
e[x].ep(y); e[y].ep(x);
}
ans=0; rt=1;
dfs(rt, 0);
printf("%lld\n", ans);
}
return 0;
}
[CF2046D] For the Emperor!
0.刚看到题
首先(你要先看到 directed 而不是 undirected)缩点,那么一个信使就可以把整个 scc 内的点传达完。
那么剩下的问题就变成了在 DAG 上的原问题。
1.解的存在性
先考虑什么时候无解,数据范围这么小,启发我们用网络流解决。
但这个模型并不是单纯的路径覆盖模型,感觉不是很好用最大流解决,那么试着带上边权,在保证满流时通过最大/小费用判断解的存在性。
我们希望信使尽可能经过不同的点,这个尽可能可以用极值描述,即如果经过一个点则会获得一个足够大/小值。我们这里讨论获得足够小值 \(-\infty\)。
先考虑每个点只有 \(a_x\) (对于 scc 则是内部点 \(a\) 之和)个信使,那么从原点给每个点连 \((S, x, a_x, 0)\)。
为了让有信使经过该点时只贡献一次 \(-\infty\),我们对每个点建替身点 \(x^\prime\),连边 \((x, x^\prime, 1, -\infty)\) 和 \((x, x^\prime, \infty, 0)\),因为费用流会优先用掉这个 \(-\infty\) 的流量,再用剩下的流量。
之后给每个原图有向边 \((x, y)\) 连 \((x^\prime, y, \infty, 0)\),再连 \((x^\prime, T, \infty, 0)\) 保证满流,就可以根据求出的最小费用判断经过了多少个点,只有经过了所有点才存在解。
2.最优解
上述建模很难取代,我们可以在此基础上再新建每个点的替身点来最优化新增初始信使的代价。
新建替身点 \(x_0\),把原来的 \((S, x, a_x, 0)\) 换成 \((S, x_0, a_x, 0)\)。
之后通过 \((x_0, x, 1, 1)\) 和 \((x_0, x^\prime, \infty, 0)\) 来控制在该城市新增初始信使时只会产生一次 \(1\) 费用,跑完费用流对 \(\infty\) 取余那部分就是最优解了。
容易发现图中没有负环,跑 Bellman-Ford 费用流即可。
图的点数是 \(O(3n)\),边数是 \(O(5n+m)\),费用流部分复杂度是 \(O(n(n+m)f)\),也就是 \(O(n^3(n+m))\),其余部分是线性。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=605, M=1e6, inf=1e9;
const ll inf2=1e18;
int Test, n, m;
int a[N];
bool f[N][N], g[N][N];
int dep[N];
ll dis[N];
int S, T;
int lin[N], nxt[M], to[M], val[M], w[M], cur[N];
int tot;
inline void in(int x, int y, int z, int _w){
nxt[++tot]=lin[x]; lin[x]=tot; to[tot]=y; val[tot]=z; w[tot]=_w;
}
inline void link(int x, int y, int z, int _w){
in(x, y, z, _w); in(y, x, 0, -_w);
}
bool v[N], vis[N];
inline bool bfs(){
for(int i=1; i<=T; ++i) dep[i]=0, dis[i]=inf2, v[i]=vis[i]=0, cur[i]=lin[i];
deque<int> q;
q.push_back(S); dis[S]=0; dep[S]=1;
while(!q.empty()){
int x=q.front(); q.pop_front();
v[x]=0;
for(int i=lin[x]; i; i=nxt[i]){
int y=to[i];
if(val[i]&&dis[y]>dis[x]+w[i]){
dis[y]=dis[x]+w[i];
dep[y]=dep[x]+1;
if(!v[y]){
v[y]=1;
if(!q.empty()&&dis[y]<dis[q.front()]) q.push_front(y);
else q.push_back(y);
}
}
}
}
return dis[T]<inf2;
}
int totflow; ll totcost;
inline int dfs(int x, int now){
if(x==T) {
totflow+=now; totcost+=dis[T]*now;
return now;
}
vis[x]=1;
int ret=0;
for(int i=cur[x]; i&&ret<now; i=nxt[i]){
int y=to[i];
cur[x]=i;
if(!vis[y]&&val[i]&&dep[y]==dep[x]+1&&dis[y]==dis[x]+w[i]){
int res=dfs(y, min(val[i], now-ret));
if(res==0) dep[y]=0;
val[i]-=res;
val[i^1]+=res;
ret+=res;
}
}
if(ret==now) vis[x]=0;
return ret;
}
int fa[N];
inline int get(int x){
while(x!=fa[x]) x=fa[x]=fa[fa[x]];
return x;
}
inline void merg(int x, int y){
x=get(x); y=get(y);
if(x==y) return ;
fa[x]=y;
}
int sum[N];
int dfn[N], low[N], timer, stk[N], top, col[N], cnt;
bool ins[N];
inline void tj(int x){
dfn[x]=low[x]=++timer;
stk[++top]=x; ins[x]=1;
for(int y=1; y<=n; ++y) if(f[x][y]){
if(!dfn[y]) tj(y), low[x]=min(low[x], low[y]);
else if(ins[y]) low[x]=min(low[x], dfn[y]);
}
if(low[x]==dfn[x]){
++cnt;
do{
col[x]=cnt;
x=stk[top--]; ins[x]=0;
}while(low[x]!=dfn[x]);
}
}
int main(){
// freopen("D:\\nya\\acm\\C\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\C\\test.out","w",stdout);
read(Test);
while(Test--){
read(n); read(m);
for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j) f[i][j]=0, g[i][j]=0;
S=3*n+1, T=S+1;
for(int i=1; i<=T; ++i) lin[i]=0;
for(int i=1; i<=n; ++i) sum[i]=0, dfn[i]=low[i]=col[i]=ins[i]=0;
tot=1; timer=cnt=top=0;
for(int i=1; i<=n; ++i) read(a[i]);
for(int i=1, x, y; i<=m; ++i) {
read(x); read(y);
f[x][y]=1;
}
for(int i=1; i<=n; ++i) fa[i]=i;
for(int i=1; i<=n; ++i) if(!dfn[i]) tj(i);
for(int i=1; i<=n; ++i) sum[col[i]]+=a[i];
for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j){
if(f[i][j]) g[col[i]][col[j]]=1;
}
for(int i=1; i<=cnt; ++i) {
link(S, i, sum[i], 0); link(i+n*2, T, inf, 0);
link(i+n, i+n*2, 1, -inf); link(i+n, i+n*2, inf, 0);
link(i, i+n, 1, 1); link(i, i+n*2, inf, 0);
for(int j=1; j<=cnt; ++j) if(i!=j){
if(g[i][j]) link(i+n*2, j+n, inf, 0);
}
}
totflow=0; totcost=0;
while(bfs()) dfs(S, inf);
if(totcost==0){
printf("-1\n");
continue;
}
int res=(-totcost-1)/inf+1;
if(res!=cnt){
printf("-1\n");
continue;
}
printf("%lld\n", (ll)res*inf+totcost);
}
return 0;
}
[CF1037H] Security
0.刚看到题
考虑怎么暴力求解。显然合法的串中与 \(t\) 匹配长度更大字典序更小,所以可以拿出来区间内的所有子串,找出和 \(t\) 匹配最多的那些,再找字典序最小的,其实就是在匹配的末尾添加一个字符。
1.没有区间限制
\(O(\sum |t|)\) 的枚举是可以被接受的,于是可以在 \(s\) 的 SAM 上先贪心匹配 \(t\),在每个可能的长度末尾加一个更大的字符,判断是否还是原串的子串,复杂度是 \(O(|s|+|\sum|\sum |t|)\)。
2.有区间限制
那么就需要我们上述贪心过程得到的串都处于区间内,可以使用 right 集合进行判断,right 集合可以用线段树合并维护。
复杂度加一个线段树的复杂度,为 \(O(|s|\log |s|+|\sum|\sum |t|\log |s|)\) 。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=2e5+5;
int n, m, T;
char s[N];
int cnt=1, lst=1;
int len[N], fa[N], ch[N][26];
vector<int> e[N];
bool tr[N*80];
int ls[N*80], rs[N*80];
int rt[N], idx;
void ins(int &p, int l, int r, int x){
if(!p) p=++idx;
tr[p]=1;
if(l==r) return ;
int mid=(l+r)>>1;
if(x<=mid) ins(ls[p], l, mid, x);
else ins(rs[p], mid+1, r, x);
}
void extend(int c, int x){
int p=lst, np=lst=++cnt;
len[np]=len[p]+1;
ins(rt[np], 1, n, x);
for(; p&&!ch[p][c]; p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else{
int nq=++cnt;
for(int i=0; i<26; ++i) ch[nq][i]=ch[q][i];
fa[nq]=fa[q];
len[nq]=len[p]+1;
fa[q]=fa[np]=nq;
for(; p&&ch[p][c]==q; p=fa[p]) ch[p][c]=nq;
}
}
}
int merge(int p, int q, int l, int r){
if(!p||!q) return p+q;
int t=++idx;
if(l==r){
tr[t]=tr[p]|tr[q];
return t;
}
int mid=(l+r)>>1;
ls[t]=merge(ls[p], ls[q], l, mid);
rs[t]=merge(rs[p], rs[q], mid+1, r);
tr[t]=tr[ls[t]]|tr[rs[t]];
return t;
}
bool get(int p, int l, int r, int L, int R){
if(!tr[p]) return false;
if(L<=l&&r<=R) return true;
int mid=(l+r)>>1; bool res=0;
if(L<=mid) res=get(ls[p], l, mid, L, R);
if(!res&&R>mid) res=get(rs[p], mid+1, r, L, R);
return res;
}
char t[N];
vector<char> ans;
int l, r;
bool work(int len, int pos){
if(len>m) {
for(int i=0; i<26; ++i){
if(ch[pos][i]&&get(rt[ch[pos][i]], 1, n, l+len-1, r)){
ans.ep(i+'a');
return true;
}
}
return false;
}
if(ch[pos][t[len]-'a']&&get(rt[ch[pos][t[len]-'a']], 1, n, l+len-1, r)&&work(len+1, ch[pos][t[len]-'a'])){
ans.ep(t[len]);
return true;
}
for(int i=t[len]-'a'+1; i<26; ++i){
if(ch[pos][i]&&get(rt[ch[pos][i]], 1, n, l+len-1, r)){
ans.ep(i+'a');
return true;
}
}
return false;
}
void dfs(int x){
for(int y:e[x]) dfs(y), rt[x]=merge(rt[x], rt[y], 1, n);
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
scanf("%s", s+1);
n=strlen(s+1);
for(int i=1; i<=n; ++i) extend(s[i]-'a', i);
for(int i=2; i<=cnt; ++i) e[fa[i]].ep(i);
dfs(1);
read(T);
while(T--){
read(l); read(r); scanf("%s", t+1); m=strlen(t+1);
ans.clear();
if(!work(1, 1)) printf("-1");
else{
reverse(ans.begin(), ans.end());
for(char c:ans) putchar(c);
}
putchar('\n');
}
return 0;
}
[CF932G] Palindrome Partition
1.第一步转化
将字符串拼接为 \(t=s_1s_ns_2s_{n-1}\cdots s_{\frac{n}{2}}s_{\frac{n}{2}+1}\) 的形式,\(s\) 的划分方式对应 \(t\) 的一个偶数长度回文划分,问题转化成新串 \(t\) 的偶数长度回文划分方案数。
朴素 dp 可以借助 PAM 枚举回文后缀,将偶数长度的进行转移即可。
2.回文后缀与 border
Lemma 1
回文串的所有回文后缀与其 border 一一对应。
证明
两个条件均等价于 \(s_i=s_{|s|-i+1}=s_{|s|-(|t|-i+1)+1}=s_{|s|-|t|+i}\)。
Lemma 2
border 的下标从小到大划分为 \(O(\log n)\) 个等差数列。
证明较难,略。
Definition 1
定义 \(len_x\) 表示 PAM 上状态 \(x\) 所代表回文串的长度。
定义 \(diff_x:=len_x-len_{fail_x}\),即 fail 树上相邻两个 border 长度差。
定义 \(slink_x\) 表示 fail 树上第一个和 \(x\) 的 \(diff\) 值不同的 \(x\) 的祖先。
根据以上引理可以得到跳 \(slink\) 指针最多 \(O(\log n)\) 次就会到达回文树根。
所以只需要在每个等差数列的位置维护等差数列从此处往上的 dp 值即可,拓展新状态时需要增量更新,详见 [cnblogs]【CF932G】Palindrome Partition(回文树,动态规划)- 小蒟蒻yyb。
根据上述分析,复杂度为 \(O(n\log n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=1e6+5, mod=1e9+7;
inline int plu(int x, int y){x+=y; return (x>=mod)?(x-mod):x;}
int n;
char s[N], t[N];
int fail[N], len[N], ch[N][26], diff[N], slink[N];
int lst=0, cnt=1;
void init(){
fail[0]=1; fail[1]=0;
len[0]=0; len[1]=-1;
}
int get_fail(int x, int lim){
while(s[lim-len[x]-1]!=s[lim]) x=fail[x];
return x;
}
void ins(int c, int lim){
int p=get_fail(lst, lim);
if(!ch[p][c]){
len[++cnt]=len[p]+2;
int q=get_fail(fail[p], lim);
fail[cnt]=ch[q][c];
ch[p][c]=cnt;
diff[cnt]=len[cnt]-len[fail[cnt]];
slink[cnt]=(diff[cnt]==diff[fail[cnt]])?slink[fail[cnt]]:fail[cnt];
}
lst=ch[p][c];
}
int f[N], g[N];
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
scanf("%s", t+1);
n=strlen(t+1);
int lp=1, rp=n;
for(int i=1; i<=n; ++i) s[i]=(i&1)?t[lp++]:t[rp--];
init();
f[0]=1;
for(int i=1; i<=n; ++i) {
ins(s[i]-'a', i);
for(int p=lst; p; p=slink[p]){
g[p]=f[i-len[slink[p]]-diff[p]];
if(fail[p]^slink[p]) g[p]=plu(g[p], g[fail[p]]);
if(!(i&1)) f[i]=plu(f[i], g[p]);
}
}
printf("%d\n", f[n]);
return 0;
}
[WC2016] 论战捆竹竿
0.刚看到题
显然是每次在后面添加一个删去 border 的原串,问题变为了求背包方案数。
1.同余最短路
背包方案数有经典的同余最短路做法,详见 [OIWIKI] 同余最短路。
本题如果朴素去做复杂度会达到 \(O(n^2\log n)\),不能接受。
2.border 理论
- 一个字符串的 border 按下标排列,可以从小到大划分为 \(O(\log n)\) 个等差数列。
每一项用 \(n\) 减去后显然也是 \(O(\log n)\) 个等差数列。
对于同一个等差数列,将原图划分为了 \(\gcd (环长, 公差)\) 个不交的环,在环上可以直接用单调队列转移。
对于相邻的两个等差数列,可以直接暴力做一遍转移,更新到新模数。
实现参考了 [洛谷] 题解 P4156【[WC2016]论战捆竹竿】 - whiteqwq
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=5e5+5;
int T, n; ll w;
char s[N];
int nxt[N];
int p[N], m;
void kmp(){
m=0;
for(int i=2, j=0; i<=n; ++i){
while(j&&s[j+1]!=s[i]) j=nxt[j];
if(s[j+1]==s[i]) ++j;
nxt[i]=j;
}
for(int i=nxt[n]; i; i=nxt[i]) p[++m]=n-i;
p[++m]=n;
}
ll dis[N], tem[N];
int lstmod;
pair<int, ll> que[N];
void work(int pos, int st, int dt, int len){
int curmod=p[pos], d=__gcd(dt, curmod);
lstmod=curmod;
for(int i=0; i<d; ++i){
int it=i;
for(int j=(i+dt)%curmod; j!=i; j=(j+dt)%curmod){
if(dis[j]<dis[it]) it=j;
}
int l=1, r=1;
que[r]=mapa(0, dis[it]);
for(int j=(it+dt)%curmod, k=1; j!=it; j=(j+dt)%curmod, ++k){
while(l<=r&&k-que[l].fi>len) ++l;
dis[j]=min(dis[j], que[l].se+1ll*(k-que[l].fi)*dt+st);
while(l<=r&&que[r].se+1ll*(k-que[r].fi)*dt>=dis[j]) --r;
que[++r]=mapa(k, dis[j]);
}
}
}
void solve(){
read(n); read(w);
scanf("%s", s+1);
kmp();
for(int i=0; i<p[1]; ++i) dis[i]=w+1;
lstmod=p[1];
dis[n%p[1]]=n;
for(int l=1, r; l<m; l=r+1){
r=l+1;
while(r<=m&&p[r]-p[r-1]==p[l+1]-p[l]) ++r;
--r;
if(l!=r) work(l, p[l], p[l+1]-p[l], r-l);
if(r!=m){
for(int i=0; i<p[r+1]; ++i) tem[i]=w+1;
for(int i=0; i<p[l]; ++i) tem[dis[i]%p[r+1]]=min(tem[dis[i]%p[r+1]], dis[i]);
for(int i=0; i<p[r+1]; ++i) dis[i]=tem[i];
work(r+1, 0, p[l], 1);
}
}
ll ans=0;
for(int i=0; i<lstmod; ++i) if(dis[i]<=w) ans+=(w-dis[i])/lstmod+1;
printf("%lld\n", ans);
}
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(T);
while(T--) solve();
return 0;
}
[2019 十二省联合省队选拔 Day 1] 字符串问题
0.刚看到题
建图后等价于找最长路,有非 0 环则输出 -1。
建图依赖前缀关系,直接后缀树优化建图即可。
特别的,需要考虑压缩状态的长度关系,所以需要开主席树优化建图,在后缀树上跑线段树合并。
容易发现图上如果有环一定是 A 串连出来的,一定是非 0 环,所以只需要判环后跑 DAG 最长路。
一发直接过了,没什么好说的,时代的眼泪,现代的入门组算法。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double dou;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mapa make_pair
typedef long double ld;
typedef unsigned long long ull;
#define ep emplace_back
template <typename T>inline void read(T &x){
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N=4e5+5;
int st;
int T, n, na, nb, m;
char s[N];
int la[N], ra[N], lb[N], rb[N];
int len[N], fa[N], ch[N][26];
int lst, cnt;
int pos[N];
void extend(int c, int x){
int p=lst, np=lst=++cnt;
len[np]=len[p]+1;
pos[x]=np;
for(; p&&!ch[p][c]; p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else{
int nq=++cnt;
for(int i=0; i<26; ++i) ch[nq][i]=ch[q][i];
len[nq]=len[p]+1;
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(; p&&ch[p][c]==q; p=fa[p]) ch[p][c]=nq;
}
}
}
vector<int> e[N];
vector<pii> g[N*30];
int f[N][20];
void dfs(int x){
for(int y:e[x]){
f[y][0]=x;
for(int i=1; f[y][i-1]; ++i) f[y][i]=f[f[y][i-1]][i-1];
dfs(y);
}
}
int locate(int l, int r){
int x=pos[r];
for(int i=19; i>=0; --i) if(len[f[x][i]]>=r-l+1) x=f[x][i];
return x;
}
int loca[N], locb[N];
int idx;
int rt[N], ls[N*50], rs[N*50];
void ins(int p, int l, int r, int x, int id){
if(l==r){
g[p].ep(id, x);
return ;
}
int mid=(l+r)>>1;
if(x<=mid){
if(!ls[p]) ls[p]=++idx, g[p].ep(ls[p], 0);
ins(ls[p], l, mid, x, id);
}
else{
if(!rs[p]) rs[p]=++idx, g[p].ep(rs[p], 0);
ins(rs[p], mid+1, r, x, id);
}
}
int merge(int p, int q, int l, int r){
if((!p)||(!q)) return p+q;
int np=++idx;
g[np].ep(p, 0); g[np].ep(q, 0);
if(l==r) return np;
int mid=(l+r)>>1;
ls[np]=merge(ls[p], ls[q], l, mid);
rs[np]=merge(rs[p], rs[q], mid+1, r);
if(ls[np]) g[np].ep(ls[np], 0);
if(rs[np]) g[np].ep(rs[np], 0);
return np;
}
void dfs2(int x){
for(int y:e[x]) dfs2(y), rt[x]=merge(rt[x], rt[y], 1, n);
}
void ins2(int p, int l, int r, int L, int R, int id){
if(!p) return ;
if(L<=l&&r<=R){
g[id].ep(p, 0);
return ;
}
int mid=(l+r)>>1;
if(L<=mid) ins2(ls[p], l, mid, L, R, id);
if(R>mid) ins2(rs[p], mid+1, r, L, R, id);
}
ll dp[N*50];
bool vis[N*50];
int que[N*50], hh, tt;
int deg[N*50];
void topo(){
for(int i=1; i<=idx; ++i) dp[i]=0, deg[i]=0;
for(int i=1; i<=idx; ++i) for(pii edg:g[i]) ++deg[edg.fi];
hh=1; tt=0;
for(int i=1; i<=idx; ++i) {
if(!deg[i]) que[++tt]=i;
}
ll ans=0;
while(hh<=tt){
int x=que[hh++];
ans=max(ans, dp[x]);
for(auto edg:g[x]){
dp[edg.fi]=max(dp[edg.fi], dp[x]+edg.se);
if(!(--deg[edg.fi])) que[++tt]=edg.fi;
}
}
for(int i=1; i<=idx; ++i) if(deg[i]!=0){
printf("-1\n"); return ;
}
printf("%lld\n", ans);
}
void solve(){
for(int i=1; i<=idx; ++i) g[i].clear(), ls[i]=rs[i]=0;
for(int i=1; i<=cnt; ++i){
for(int c=0; c<26; ++c) ch[i][c]=0;
for(int t=0; t<20; ++t) f[i][t]=0;
len[i]=0; fa[i]=0; e[i].clear();
}
lst=cnt=1;
scanf("%s", s+1);
n=strlen(s+1);
reverse(s+1, s+n+1);
for(int i=1; i<=n; ++i) extend(s[i]-'a', i);
for(int i=2; i<=cnt; ++i) e[fa[i]].ep(i);
dfs(1);
read(na);
for(int i=1; i<=na; ++i) read(ra[i]), read(la[i]);
read(nb);
for(int i=1; i<=nb; ++i) read(rb[i]), read(lb[i]);
idx=cnt+na+nb;
for(int i=1, l, r; i<=na; ++i){
r=ra[i]; l=la[i]; r=n+1-r; l=n+1-l;
loca[i]=locate(l, r);
ins(loca[i], 1, n, r-l+1, i+cnt);
}
for(int i=1; i<=cnt; ++i) rt[i]=i;
dfs2(1);
for(int i=1, l, r; i<=nb; ++i){
r=rb[i]; l=lb[i]; r=n+1-r; l=n+1-l;
locb[i]=locate(l, r);
ins2(rt[locb[i]], 1, n, r-l+1, n, i+cnt+na);
}
read(m);
for(int i=1, x, y; i<=m; ++i){
read(x); read(y);
g[x+cnt].ep(y+cnt+na, 0);
}
topo();
}
int ed;
int main(){
// freopen("D:\\nya\\acm\\A\\test.in","r",stdin);
// freopen("D:\\nya\\acm\\A\\test.out","w",stdout);
read(T);
while(T--) solve();
cerr<<1.0*((&st)-(&ed)+1)/1024;
return 0;
}

浙公网安备 33010602011771号