2024.10.04 刷题记录
2024.10.04 刷题记录
P6764 APIO2020 粉刷墙壁
不难发现题目大意是每次选一个点,向后覆盖一个长度为 \(m\) 的段。
那我们只关心从点 \(i\) 开始能不能向后覆盖长度大于等于 \(m\) 的距离。
设 \(f[i][j]\) 表示第 \(i\) 段墙,被第 \(j\) 个承包商刷的最大可以向后覆盖的段数(连续,可以刷多次,每次起点在上一次的区间内)。
转移:
其中第 \(j\) 个承包商喜欢第 \(c_i\) 个颜色。
由于喜欢一个颜色的承包商小于 \(\sum f(k) \leq 200\) 个,复杂度 \(O(n\sum f(k))\)。
需要进行滚动数组优化空间。
算要求数,可以转化为拼接线段的模型,每次选出一个起点之后,前一个起点一定在 \([i-M+1,i)\) 这个区间内,暴力跑出最远位置即可。
// #include"paint.h"
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e5+3;
int col[maxn],p[maxn],dp[2][maxn];
vector<int> s[maxn];
int minimumInstructions(int n,int m,int k,vector<int> c,vector<int> a,vector<vector<int>> b)
{
int ans=0;
for(int i=1;i<=n;++i) col[i]=c[i-1]+1;
for(int i=1;i<=m;++i) for(int j:b[i-1]) s[j+1].push_back(i);
for(int x,i=n;x=i&1,i;--i)
{
for(int j:s[col[i]]) p[i]|=(dp[x][j]=dp[!x][j%m+1]+1)>=m;
for(int j:s[col[i+1]]) dp[!x][j]=0;
}
for(int i=n-m+1;i;--i)
{
if(++ans,!p[i]) return -1;
for(int j=max(i-m,1);j<i;++j)
{
if(p[j])
{
i=j+1;
break;
}
}
}
return ans;
}
// int main()
// {
// int n,m,k;
// scanf("%d%d%d",&n,&m,&k);
// vector<int>c(n),a(m);
// vector<vector<int>>b(m);
// for(int i=0;i<n;++i) scanf("%d",&c[i]);
// for(int i=0;i<m;++i)
// {
// scanf("%d",&a[i]);b[i].resize(a[i]);
// for(int j=0;j<a[i];j++) scanf("%d",&b[i][j]);
// }
// printf("%d",minimumInstructions(n,m,k,c,a,b));
// return 0;
// }
P7929 COCI2021-2022#1 Logičari
基环树 \(dp\),从更方便的角度思考,看做树和多出来的一条边,要注意一个被染色的点周围也要有一个点被染色。
考虑树证明处理,设 \(dp[i][0/1][0/1]\) 表示第 \(i\) 个点是否染色,\(i\) 是否有一个儿子染色。
有转移:
对于叶子节点:\(dp[u][0][0]=0,dp[u][0][1]=inf,dp[u][1][0]=1,dp[u][1][1]=inf\)。
接着分类讨论一下通过多出来的边连接的两个点,其中其中一个为根(\(rt\))(确实也要从 \(rt\) 开始 \(dp\)),一个为根的兄弟(\(rt-bro\))。
下面的 \(1\) 表示被染色,\(0\) 表示未被染色。
-
\(rt=1,rt-bro=0\):
\(dp[rt-bro][1][1]=inf,dp[rt-bro][1][0]=inf,dp[rt-bro][0][1]=dp[rt-bro][0][0],dp[rt-bro][0][0]=inf\)。
其中 \(dp[rt-bro][0][1]=dp[rt-bro][0][0]\) 表示 \(dp[rt-bro][0][1]\) 需要通过 \(dp[rt-bro][0][0]\) 的方程转移,下同。
答案为 \(dp[rt][1][1]\)。
-
\(rt=1,rt-bro=1\):
\(dp[rt-bro][1][1]=dp[rt-bro][1][0],dp[rt-bro][1][0]=inf,dp[rt-bro][0][1]=inf,dp[rt-bro][0][0]=inf\)。
答案为 \(dp[rt][1][0]\)。
-
\(rt=0,rt-bro=0\):
\(dp[rt-bro][1][1]=inf,dp[rt-bro][1][0]=inf\)。
答案为 \(dp[rt][0][1]\)。
-
\(rt=0,rt-bro=1\):
\(dp[rt-bro][0][1]=inf,dp[rt-bro][0][0]=inf\)。
答案为 \(dp[rt][0][0]\)。
若上述四种情况均为 \(inf\),则无解。
否则取最小值即可。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 1e9
const int maxn=1e5+5;
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt;}edge[maxn*2];
inline void add(int x,int y)
{
tot++;
edge[tot].to=y;
edge[tot].nxt=head[x];
head[x]=tot;
}
}T;
int n,rt,rt_bro,flg;
int f[maxn];
ll ans=inf;
ll dp[maxn][2][2];
inline int fr(int u){return f[u]==u?u:f[u]=fr(f[u]);}
inline void dfs(int u,int f)
{
dp[u][0][1]=dp[u][1][1]=inf;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(v==f) continue;
dfs(v,u);
dp[u][0][0]+=dp[v][0][1];
if(dp[v][1][1]<inf) dp[u][0][1]=min(dp[u][0][1],dp[v][1][1]-dp[v][0][1]);
dp[u][1][0]+=dp[v][0][0];
if(dp[v][1][0]<inf) dp[u][1][1]=min(dp[u][1][1],dp[v][1][0]-dp[v][0][0]);
}
dp[u][0][1]=dp[u][0][0]+dp[u][0][1];
dp[u][1][0]++;
dp[u][1][1]=dp[u][1][0]+dp[u][1][1];
if(u==rt_bro)
{
if(flg==1)
{
dp[u][1][1]=inf,dp[u][1][0]=inf,dp[u][0][1]=dp[u][0][0],dp[u][0][0]=inf;
}
else if(flg==2)
{
dp[u][1][1]=dp[u][1][0],dp[u][1][0]=inf,dp[u][0][1]=inf,dp[u][0][0]=inf;
}
else if(flg==3)
{
dp[u][1][0]=inf,dp[u][1][1]=inf;
}
else
{
dp[u][0][0]=inf,dp[u][0][1]=inf;
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
int fu=fr(u),fv=fr(v);
if(fu==fv) {rt=u,rt_bro=v;continue;}
T.add(u,v),T.add(v,u);
f[fu]=fv;
}
memset(dp,0,sizeof(dp));
flg=1;
dfs(rt,0);
ans=min(ans,dp[rt][1][1]);
memset(dp,0,sizeof(dp));
flg=2;
dfs(rt,0);
ans=min(ans,dp[rt][1][0]);
memset(dp,0,sizeof(dp));
flg=3;
dfs(rt,0);
ans=min(ans,dp[rt][0][1]);
memset(dp,0,sizeof(dp));
flg=4;
dfs(rt,0);
ans=min(ans,dp[rt][0][0]);
printf("%lld",ans<inf?ans:-1);
}
P10044 CCPC 2023 北京市赛 最小环
诈骗题。
先拓扑把出度或入度为 \(0\) 的点全部删除,然后对于剩下的点,如果出度入度就进行缩边,边权等于原来两条边的边权和。
剩下的点至少是三度点,而一条边只能提供两度,满足 \(3n\leq 2m\)。
结合 \(m-n\leq 1500\),解得 \(n\leq 4000,m\leq 4500\)。
这里的 \(m,n\) 都是新图中的,所以只需要建出新图每个点跑 dij,然后枚举边成环即可。
需要特殊处理一下度为 \(2\) 的点成环的情况。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long
#define pli pair<ll,int>
#define fi first
#define se second
const int maxn=3e5+5;
inline int read()
{
int x;
x=0;bool flag(0);char ch=getchar();
while(!isdigit(ch)) flag=ch=='-',ch=getchar();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
flag?x=-x:0;
return x;
}
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt;ll w;}edge[maxn*2];
inline void add(int x,int y,int z)
{
tot++;
edge[tot].to=y;
edge[tot].w=z;
edge[tot].nxt=head[x];
head[x]=tot;
}
}G,fG;
int n,m;
int from[maxn],rd[maxn],cd[maxn];
bool vis[maxn],del[maxn];
ll ans=1e18;
ll dis[maxn];
inline void dij(int st)
{
for(int i=0;i<=n;i++) vis[i]=false,dis[i]=1e18;
dis[st]=0;
priority_queue<pli,vector<pli>,greater<pli>>que;
que.push({0,st});
while(!que.empty())
{
int u=que.top().se;que.pop();
if(vis[u]) continue;
vis[u]=true;
for(int i=G.head[u];i;i=G.edge[i].nxt)
{
int v=G.edge[i].to;
if(dis[v]>dis[u]+G.edge[i].w)
{
dis[v]=dis[u]+G.edge[i].w;
que.push({dis[v],v});
}
}
}
}
inline void bfs()
{
queue<int>que;
for(int i=1;i<=n;i++) if(!rd[i]||!cd[i]) que.push(i),vis[i]=true;
while(!que.empty())
{
int u=que.front();que.pop();
if(!rd[u])
for(int i=G.head[u];i;i=G.edge[i].nxt)
{
int v=G.edge[i].to;
rd[v]--;
if(!vis[v]&&!rd[v]) que.push(v),vis[v]=true;
}
else
for(int i=fG.head[u];i;i=fG.edge[i].nxt)
{
int v=fG.edge[i].to;
cd[v]--;
if(!vis[v]&&!cd[v]) que.push(v),vis[v]=true;
}
}
}
signed main()
{
// scanf("%d%d",&n,&m);
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u,v,w;
// scanf("%d%d%d",&u,&v,&w);
u=read(),v=read(),w=read();
if(u==v) {ans=min(ans,(ll)w);continue;}
cd[u]++,rd[v]++;
G.add(u,v,w);
fG.add(v,u,G.tot);
}
bfs();
for(int i=1;i<=n;i++)
{
if(rd[i]==1&&cd[i]==1)
{
for(int j=fG.head[i];j;j=fG.edge[j].nxt)
{
int v=fG.edge[j].to;
if(!vis[v]){from[i]=fG.edge[j].w;break;}
}
for(int j=G.head[i];j;j=G.edge[j].nxt)
{
int v=G.edge[j].to;
if(!vis[v]){G.head[i]=j;break;}
}
}
}
for(int i=1;i<=n;i++)
{
if(cd[i]==1&&rd[i]==1&&G.edge[G.head[i]].to!=i)
{
G.edge[from[i]].to=G.edge[G.head[i]].to;
G.edge[from[i]].w+=G.edge[G.head[i]].w;
from[G.edge[G.head[i]].to]=from[i];
del[i]=true;
}
}
for(int u=1;u<=n;u++) for(int j=G.head[u];j;j=G.edge[j].nxt)
{
int v=G.edge[j].to;
if(v==u) ans=min(ans,G.edge[j].w);
}
int cnt=0;
for(int i=1;i<=n;i++)
{
if(rd[i]&&cd[i]&&!del[i])
{
cnt++;
assert(cnt<=4000);
dij(i);
for(int u=1;u<=n;u++) if(dis[u]^dis[0]) for(int j=G.head[u];j;j=G.edge[j].nxt)
{
int v=G.edge[j].to;
if(v==i) ans=min(ans,dis[u]+G.edge[j].w);
}
}
}
printf("%lld",ans==1e18?-1:ans);
}
P6604 HNOI2016 序列 加强版
P3246 HNOI2016 序列
转图论的笛卡尔树做法。
设 \(f_i\) 为以 \(i\) 为右端点,任意左端点的贡献和。
设 \(p\) 为满足 \(a_p< a_i,q<i\),的最大的 \(p\)。
这里的 \(p\) 极其类似笛卡尔树中第一个向左走的父亲,这里可以用类笛卡尔树的方式求出 \(p\)(当然其他更简单的方法也可以)。
有 \(f_i=f_p+(i-p)\times a_i\)。
考虑一个查询,找到查询区间中最小的点 \(p\),有贡献 \((p-l+1)\times(r-p+1)\times a_p\)。
对于区间 \((p,r]\) 每个点的贡献都是 \(f_i-f_p\),因为 \(f_i\) 中区间 \([j,i](1\leq j\leq p)\) 的贡献来源于 \(f_p\)。
对于 \([l,p)\) 序列反过来求过 \(g\) 即可。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1e5+5;
namespace gen{
typedef unsigned long long ull;
ull s,a,b,c,lastans=0;
ull rand(){
return s^=(a+b*lastans)%c;
}
};
int n,m,tp;
int a[maxn],st[maxn][25],pre[maxn],nxt[maxn];
ll f[maxn],sf[maxn],g[maxn],sg[maxn];
inline int cmp(int x,int y){return a[x]<a[y]?x:y;}
inline int qry(int l,int r)
{
int k=__lg(r-l+1);
return cmp(st[l][k],st[r-(1<<k)+1][k]);
}
int main()
{
a[0]=2e9;
scanf("%d%d%d",&n,&m,&tp);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),st[i][0]=i;
for(int i=1;i<=20;i++) for(int j=1;j+(1<<i)-1<=n;j++)
st[j][i]=cmp(st[j][i-1],st[j+(1<<(i-1))][i-1]);
stack<int>stk;
for(int i=1;i<=n;i++)
{
while(!stk.empty())
{
if(a[stk.top()]>=a[i]) nxt[stk.top()]=i,stk.pop();
else break;
}
if(!stk.empty()) pre[i]=stk.top();
stk.push(i);
}
for(int i=1;i<=n;i++) f[i]=f[pre[i]]+1ll*a[i]*(i-pre[i]),sf[i]=sf[i-1]+f[i];
for(int i=n;i;i--) g[i]=g[nxt[i]]+1ll*a[i]*(nxt[i]-i),sg[i]=sg[i+1]+g[i];
if(tp) cin>>gen::s>>gen::a>>gen::b>>gen::c;
unsigned ll res=0;
for(int i=1,l,r;i<=m;i++)
{
if(tp==0) scanf("%d%d",&l,&r);
else {
l=gen::rand()%n+1;
r=gen::rand()%n+1;
if(l>r) swap(l,r);
}
ll p=qry(l,r),ans=0;
ans=a[p]*(r-p+1)*(p-l+1);
ans+=sf[r]-sf[p]-f[p]*(r-p);
ans+=sg[l]-sg[p]-g[p]*(p-l);
res^=ans;
gen::lastans=ans;
}
printf("%lld",res);
}
P6628 省选联考 2020 B 卷 丁香之路
必经 \(m\) 条边,和 \(s,i\) 两个点。
这些边和点放置于图中,此时若只有一个连通块,最优方案肯定希望 \(m\) 条边与 \(i,s\) 构成欧拉图,其中起点为 \(s\),终点为 \(i\)。
遂发现若 \(m\) 条边不构成欧拉图,加边肯定比走重复的边更合适,结合题目手玩几组即可发现。
于是如下构造,先连一条权为 \(0\) 的边联通 \(s,i\),然后从 \(1\) 开始遍历,若 \(p\) 点的度数为奇数,则向 \(p+1\) 连边,最后连出的图肯定是最小的,而且构成欧拉回路。
再考虑构造后分成了多个连通块,在连通块间求最小生成树,树边重复2次就是不同连通块的贡献(三角形的边长关系可以粗略证明)。
最后答案就是2倍树边加构造的边加最初 \(m\) 条边。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pipii pair<int,pair<int,int>>
#define fi first
#define se second
const int maxn=2505;
int n,m,s;
int bel[maxn],f[maxn],deg[maxn];
ll sum;
inline int fr(int u){return f[u]==u?u:f[u]=fr(f[u]);}
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
sum+=abs(u-v);deg[u]++,deg[v]++;
int fu=fr(u),fv=fr(v);
if(fu!=fv) f[fu]=fv;
}
for(int i=1;i<=n;i++) bel[i]=fr(i);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) f[j]=j;
deg[s]++,deg[i]++;
f[fr(bel[s])]=fr(bel[i]);
ll ans=sum;int pre=0;
for(int j=1;j<=n;j++)
{
if(deg[j]&1)
{
if(pre)
{
ans+=j-pre;
for(int k=pre;k<j;k++) f[fr(bel[k])]=fr(bel[j]);
pre=0;
}
else pre=j;
}
}
vector<pipii>vec;
for(int j=1;j<=n;j++)
{
if(deg[j])
{
if(pre&&fr(bel[j])!=fr(bel[pre])) vec.push_back({j-pre,{bel[j],bel[pre]}});
pre=j;
}
}
sort(vec.begin(),vec.end());
for(auto j:vec)
{
int u=j.se.fi,v=j.se.se;
int fu=fr(u),fv=fr(v);
if(fu!=fv) f[fu]=fv,ans+=j.fi*2;
}
deg[s]--,deg[i]--;
printf("%lld ",ans);
}
}

浙公网安备 33010602011771号