【SZOI】2021暑假集训合集
A 和 B 博弈,每次 A 将原集合划分成 份(可以为空),B 选择其中一份,将其中每个元素减 并将其它
份从原集合中删除。A 的目标是生成出 ,B 的目标是阻止 A,即将集合变为空集。
问 A 是否有必胜策略。若有,输出分配方案。
首先原问题看起来不好下手,从特殊情况出发。
考虑当集合元素全都相同时的情况。
由于元素之间不存在差异,故 B 只能选择最少的一部分。所以 A 尽量平均分。
那么易知当且仅当 时 A 能赢。( 为元素的值)
然后又是一个 trick:
如果说现在有 个 ,那么操作后变为一个 。
好好想。
然后又因为 操作后就变成了 ,所以考虑如何通过多次转化推进到 。
对于 ,至少需要 个才能得到 。
比如说有 个 就赢了,而一个 需要 个 来凑,所以需要 个 ,以此类推。
所以直接这样往前进位,查看是否得到 即可。
然后就随便操作了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
#define cl clear
const int N=1e5+6;
int T,a[N],s[N],p[N];
vector<int>vec[N],tr[N];
signed main(){
scanf("%lld",&T);
while(T--){
int n,k;
scanf("%lld%lld",&n,&k);
memset(p,0,sizeof(p));
memset(s,0,sizeof(s));
for(int i=0;i<=n;i++)vec[i].cl();
for(int i=0;i<=k;i++)tr[i].cl();
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
vec[a[i]].pb(i);
}
for(int i=1;i<=n;i++)s[a[i]]++;
for(int i=n;i>=0;i--)p[i]=s[i]+p[i+1]/k;
if(p[0]<1){
puts("0");
continue;
}
puts("1");
int res=1,cnt=1,now=1;
for(int i=1;i<=n;i++){
int sz=vec[i].size();
for(int j=0;j<sz;j++){
int x=vec[i][j];
tr[now].pb(x);
cnt--;
if(cnt)continue;
if(now<k){
now++;
cnt=res;
}
}
cnt*=k;
res*=k;
}
for(int i=1;i<=k;i++){
int sz=tr[i].size();
printf("%lld ",sz);
for(int j=0;j<sz;j++)
printf("%lld ",tr[i][j]);
puts("");
}
}
return 0;
}
有一个长度为 的字符串 ,已知 个位置上的字符。
有 对关系,每一对关系可以表示成 ,其中 ,表示 ,并且这 对关系满足 互不相交。
查询 次,每次查询第 个字符,若不可以通过关系推出则输出 ?
,不会出现冲突。
首先 看起来就很假。
所以复杂度应该和 没什么关系。
那么就把这个 给弃了。
想一想对于 的做法。
这里有一个小 trick:既然每对关系的第二个区间互不相交,那么所有的区间的总和是不会超过 的。
对于每一次我们可以暴力枚举 ,其复杂度为 。
然后这里有一个非常假的做法:每一次对于两个区间直接赋值。
解释一下为什么这个做法很假:如果说我先给你两个都未知的区间其中有两个对应数 ,之后给了另一个关系,而这个关系中包含了 ,而也可以正好推出 ,但是此时你就不能回去再推出 。
所以可以自然地想到并查集啦。
对于每次关系,将它们搞到并查集里。
总的时间复杂度 。
然后再来搞正解。
因为后面的区间是不会有相交的,说明对于每个点,它最多被一个右区间覆盖,不妨直接令它的父亲为在左区间里和它对应的那个位置。
那么这样也形成一组森林,且它也是一种合法的并查集。我们只需要在这个并查集的根上维护每个联通块是否又被确定为某一个字符,即可在询问的时候进行回答。
那么如何查找一个位置的根。
我们可以将关系给排序,然后不断地往左移动,跳出当前所在区间,直到根,然后在根上维护。
总的时间复杂度
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+6;
#define int long long
map<int,char>mps,ans;
int n,m1,m2,q,arr[N];
struct node{
int l1,r1,l2,r2;
}e[N];
inline bool cmp(node a,node b){
return a.l2<b.l2;
}
inline bool check(int l,int r,int x){
if(x<=r&&l<=x)return true;
return false;
}
inline int find(int x){
for(int i=m2;i>=1;i--){
if(check(e[i].l2,e[i].r2,x)){
int l=e[i].l1,r=e[i].l2;
x-=(x-l)/(r-l)*(r-l);
}
}
return x;
}
signed main(){
scanf("%lld%lld%lld%lld",&n,&m1,&m2,&q);
for(int i=1;i<=m1;i++){
char ch;
cin>>arr[i];
cin>>ch;
mps[i]=ch;
}
for(int i=1;i<=m2;i++)
scanf("%lld%lld%lld%lld",&e[i].l1,&e[i].r1,&e[i].l2,&e[i].r2);
sort(e+1,e+m2+1,cmp);
for(int i=1;i<=m1;i++){
int x=find(arr[i]);
ans[x]=mps[i];
}
while(q--){
int x;
scanf("%lld",&x);
int y=find(x);
if(!ans.count(y))puts("?");
else cout<<ans[y]<<endl;
}
return 0;
}
有一棵 个节点的树,每条边有边权 。
有 个询问 ,查询以 为根、能量为 时,最多能遍历的节点。定义从节点 能遍历到节点 当且仅当其路径上的 的最小值大于等于 。
。
事实上不需要去建出这棵树。
只需要记录每一条边的 即可。
而对于 个询问,其实相当于是有一从 到某个点的边,权值为 。而这个某个点就是 。然而注意这些边不能与树的边混在一起,所以建边时注意应该建比如 这样的。
考虑从 到 的节点只是由其路径上的最小 决定的,可以考虑去对于每条链维护一个最小值。
但这显然行不通。
考虑将路径的 从大到小去 sort 一遍。(包括那些询问的边)
那么再次遍历这些边。若这些边在树中,则考虑将 用并查集去 merge,然后统计一个 ,表示 所在的并查集的大小。
若是查询,则 。
由于是按照 降序排列,那么当前所遇到的边必然比之前所遇到的边要小。在 merge 的时候故不需要去考虑 的限制。
那么当前所遇到的查询,它查询出来的 必然都是满足条件 的节点个数。
所以这样是正确的。
最后将 减去自己的 即可。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,fa[N],sz[N],ans[N];
struct node{
int u,v,w;
}e[N<<1];
inline bool cmp(node a,node b){
if(a.w==b.w)return a.v>b.v;
return a.w>b.w;
}
inline int find(int x){
if(x==fa[x])return x;
return fa[x]=find(fa[x]);
}
inline void merge(int x,int y){
int fx=find(x),fy=find(y);
if(fx==fy)return;
fa[fx]=fy;
sz[fy]+=sz[fx];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
sz[i]=1;
fa[i]=i;
}
for(int i=1;i<n;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
for(int i=1;i<=m;i++){
scanf("%d%d",&e[i+n-1].w,&e[i+n-1].u);
e[i+n-1].v=-i;
}
sort(e+1,e+n+m,cmp);
for(int i=1;i<n+m;i++){
if(e[i].v>=0)merge(e[i].u,e[i].v);
else ans[-e[i].v]=sz[find(e[i].u)];
}
for(int i=1;i<=m;i++)printf("%d\n",ans[i]-1);
return 0;
}
有 个区间,去掉其中一个区间,使区间并集最大。
。
考虑求出所有区间并集。
然后求出每个区间的贡献
删除贡献最小即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+6;
const int inf=(1<<20);
int ans,res,n;
struct node{
int l,r;
}a[N];
inline bool cmp(node a,node b){
return a.l<b.l;
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld%lld",&a[i].l,&a[i].r);
sort(a+1,a+n+1,cmp);
a[n+1].l=a[n+1].r=inf;
int p=a[1].l,q=a[1].r;
for(int i=2;i<=n;i++){
int l=a[i].l,r=a[i].r;
if(l>q){
res+=q-p;
p=l;
q=r;
}
else{
p=min(p,l);
q=max(q,r);
}
}
res+=q-p;
ans=min(a[1].r-a[1].l,a[2].l-a[1].l);
for(int i=2;i<=n;i++){
int x=max(a[i-1].r,a[i].l);
ans=min(ans,min(a[i].r-x,a[i+1].l-x));
}
for(int i=2;i<=n;i++){
if(a[i].r<a[i-1].r){
printf("%lld",res);
return 0;
}
}
printf("%lld",res-ans);
return 0;
}
有一棵 个点的树,在上面建工作室,每个节点若建工作室则有 的收益。一个点仅可以建一家工作室。有两家赞助商赞助建工作室。对于其中任意一家,这棵树以这家赞助商的总部为根节点,会有一些要求,形如在以 为根的子树中仅能设置 个工作室。
问如何安排使收益最大。
。
考虑费用流。
首先分别以两家赞助商的根为节点建出两棵树。
考虑对于一棵子树,限制其工作室的个数可以转化为限制的流量。
然后对于收益,就相当于是费用。费用在树边上不好体现,考虑自己 向一个代表自己的节点 去连边,收益即为 ,而限流为 ,正好将限流转化为个数限制。
具体地说:源点向两棵树根节点连边,每个点向它儿子连边,如果它儿子有子树限制 ,边的最大流量就是这个限制 ,否则是 ,再从两棵树里的 号点分别向一个代表选择 号点的 点连边,并从 向汇点连一条流量是 ,收益是 的边,在图上跑最大费用最大流即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int N=5e4+45;
const int M=1e5+6;
const int inf=0x3f3f3f3f;
int head[N],n,m,q1,q2,s,t,cur[N],dep[N],tot=1,ans,rt1,rt2,c[N],lim[N];
bool vis[N],inq[N];
vector<int>g[M];
struct node{
int to,nxt,w,c;
}e[N<<1];
inline void addedge(int u,int v,int w,int c){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
e[tot].w=w;
e[tot].c=c;
return;
}
inline void add(int u,int v,int w,int c){
addedge(u,v,w,c);
addedge(v,u,0,-c);
return;
}
inline bool spfa(){
memcpy(cur,head,sizeof(cur));
for(int i=1;i<N;i++)dep[i]=-inf;
queue<int>q;
q.push(s);
dep[s]=0;
vis[s]=true;
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].w&&dep[v]<dep[u]+e[i].c){
dep[v]=dep[u]+e[i].c;
if(!vis[v]){
q.push(v);
vis[v]=true;
}
}
}
}
return dep[t]!=-inf;
}
inline int dfs(int u,int val){
if(u==t)return val;
vis[u]=true;
int res=0;
for(int &i=cur[u];i;i=e[i].nxt){
if(res>=val)break;
int v=e[i].to;
if(vis[v])continue;
if(e[i].w&&dep[v]==dep[u]+e[i].c){
int flow=dfs(v,min(e[i].w,val-res));
if(flow){
e[i].w-=flow;
e[i^1].w+=flow;
res+=flow;
ans+=flow*e[i].c;
}
}
}
vis[u]=false;
return res;
}
inline void dinic(){
int res=0;
while(spfa()){
int x;
while(x=dfs(s,inf))res+=x;
}
}
inline int id(int x){
return n*2+x;
}
inline void predfs(int u,int fa){
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==fa)continue;
add(u,v,lim[v],0);
predfs(v,u);
}
return;
}
signed main(){
scanf("%lld",&n);
s=0,t=3*n+2;
for(int i=1;i<=n;i++){
add(i,id(i),1,0);
add(n+i,id(i),1,0);
}
for(int i=1;i<=n;i++){
scanf("%lld",&c[i]);
add(id(i),t,1,c[i]);
}
memset(lim,0x3f,sizeof(lim));
scanf("%lld",&rt1);
for(int i=1;i<n;i++){
int u,v;
scanf("%lld%lld",&u,&v);
g[u].pb(v),g[v].pb(u);
}
scanf("%lld",&rt2),rt2+=n;
for(int i=1;i<n;i++){
int u,v;
scanf("%lld%lld",&u,&v);
u+=n,v+=n;
g[u].pb(v),g[v].pb(u);
}
scanf("%lld",&q1);
while(q1--){
int x,y;
scanf("%lld%lld",&x,&y);
lim[x]=y;
}
scanf("%lld",&q2);
while(q2--){
int x,y;
scanf("%lld%lld",&x,&y);
lim[x+n]=y;
}
add(s,rt1,lim[rt1],0),add(s,rt2,lim[rt2],0);
predfs(rt1,0),predfs(rt2,0);
dinic();
printf("%lld",ans);
return 0;
}
有 个工作室,第 个工作室有 的预算。有 条道路,每条道路连接 ,花费是 。能修建一条道路仅当 ,之后 将共用 。
希望将这 个点连接成一棵树。求是否可能、方案。
。
考虑把建边化为缩点。
对于一棵树,有解当且仅当满足点权和 边权和。
这很显然。
然后有个 naive 的做法,把图的最小生成树求出来然后按上面做。但是这样是 。
考虑在构造方案的时候省省时间,在最小生成树上 dfs 下去,定义 为 的子树的点权和加上父亲的点权减去子树边权和再减去它的父边的边权。
显然当 ,子树 能缩点。
于是这些子树先合并起来,对于 的子树将它合并之后必定能将根节点的可供用权值变大,所以我们也先缩这些子树,这时候再做那些其他的原本不能直接合并的边就行了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=6e6+1;
int tot,tot2,head[N<<1],ans[N],s,s2,n,m,fa[N],a[N],p[N],res;
bool flag[N];
struct node{
int to,nxt,from,w,id;
}e[N<<1],g[N<<1];
inline void addedge(int u,int v,int val,int id){
e[++tot].nxt=head[u];
e[tot].from=u;
e[tot].to=v;
e[tot].w=val;
e[tot].id=id;
head[u]=tot;
}
inline void addedge2(int u,int v,int val,int id){
g[++tot2].nxt=head[u];
g[tot2].from=u;
g[tot2].to=v;
g[tot2].w=val;
g[tot2].id=id;
head[u]=tot2;
}
inline int find(int x){
if(x==fa[x])return x;
fa[x]=find(fa[x]);
return fa[x];
}
inline void merge(int x,int y){
int fx=find(x),fy=find(y);
if(fx==fy)return;
fa[fx]=fy;
return;
}
inline bool cmp(node a,node b){
return a.w<b.w;
}
inline void kruscal(){
sort(e+1,e+tot+1,cmp);
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1,j=0;i<=tot;i++){
if(j>n)break;
int u=e[i].from,v=e[i].to,fau=find(u),fav=find(v);
if(fau==fav)continue;
j++;
flag[i]=true;
merge(fau,fav);
s2+=e[i].w;
}
return;
}
inline void dfs(int u,int pre){
p[u]=a[u];
for(int i=head[u];i;i=g[i].nxt){
int v=g[i].to;
if(pre==v){
p[u]-=g[i].w;
continue;
}
dfs(v,u);
p[u]+=p[v];
}
return;
}
inline void dfs2(int u,int pre){
for(int i=head[u];i;i=g[i].nxt){
int v=g[i].to;
if(pre==v)continue;
if(p[v]>=0){
dfs2(v,u);
res++;
ans[res]=g[i].id;
}
}
for(int i=head[u];i;i=g[i].nxt){
int v=g[i].to;
if(pre==v)continue;
if(p[v]<0){
if(p[v]+g[i].w<0){
res++;
ans[res]=g[i].id;
dfs2(v,u);
continue;
}
dfs2(v,u);
res++;
ans[res]=g[i].id;
}
}
return;
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
s+=a[i];
}
for(int i=1;i<=m;i++){
int u,v,val;
scanf("%lld%lld%lld",&u,&v,&val);
addedge(u,v,val,i);
addedge(v,u,val,i);
}
kruscal();
if(s<s2){
puts("0");
return 0;
}
puts("1");
memset(head,0,sizeof(head));
for(int i=1;i<=tot;i++){
if(!flag[i])continue;
addedge2(e[i].from,e[i].to,e[i].w,e[i].id);
addedge2(e[i].to,e[i].from,e[i].w,e[i].id);
}
dfs(1,0);
dfs2(1,0);
for(int i=1;i<n;i++)printf("%lld ",ans[i]);
return 0;
}
有一个序列 。
设
对于一个给定的 ,构造序列 ,设 是多少。
对于给定的 ,求有哪些 ,满足 。
同 树上染色。
给一棵 个点的树, 有边权. 你需要选出 个点染黑, 并将其他 个点染白。你需要最大化黑点两两距离和加上白点两两距离和。
。
令 表示以 作为根的子树,选出 个黑点的贡献最大值,通过边来转移。
考虑每一条边的贡献。设其一侧的黑、白点数为 ,另一侧黑、白点数为 ,边权为 。则贡献为:
。
即为
为 的子节点, 为在这个子节点中选择的黑色点的个数, 为以 为根的子树的节点数量。
那个整个问题成了一个树形背包,考虑每个子节点分配多少个黑色节点(体积),然后算出这条边对答案的贡献(价值)。
其中v为u的子节点,j为在这个子节点中选择的黑色点的个数,val为这条边的贡献
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2001;
int f[N],dp[N][N],n,m,head[N],tot;
struct node{
int nxt,to,w;
}e[N<<1];
inline void addedge(int u,int v,int w){
e[++tot].to=v;
e[tot].w=w;
e[tot].nxt=head[u];
head[u]=tot;
}
inline int dfs(int u,int fa){
dp[u][0]=dp[u][1]=0;
f[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa)continue;
f[u]+=dfs(v,u);
}
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,tr=f[v],minx=min(m,f[u]);
if(v==fa)continue;
for(int j=minx;j>=0;j--){
int minx2=min(tr,j);
for(int l=0;l<=minx2;l++){
if(dp[u][j-l]==-1)continue;
int p=m-l,w=e[i].w,x=(l*p+(tr-l)*(n-tr-p))*w;
dp[u][j]=max(dp[u][j],dp[u][j-l]+dp[v][l]+x);
}
}
}
return f[u];
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<n;i++){
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
addedge(u,v,w);
addedge(v,u,w);
}
memset(dp,-1,sizeof(dp));
dfs(1,0);
printf("%lld",dp[1][m]);
return 0;
}
给 , 求 的值, 其中 表示二进制意义下的异或。
。
数位dp。
令 表示考虑到第 位,是否顶着 上界,对应的方案数和 之和。
只考虑当 的情况, 转移。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=62;
const int lim=61;
int T,n,m,k,p,inv[N],f[2][2][2][N],g[2][2][2][N],A[N],B[N],C[N];
inline void dfs(int a,int b,int c,int x){
if(f[a][b][c][x]||(!x))return;
for(int i=0;i<=1;i++){
for(int j=0;j<=1;j++){
int l=i^j;
if((a&&(i>A[x-1]))||(b&&(j>B[x-1]))||(c&&(l<C[x-1])))continue;
int a_=(a&&(i==A[x-1])),b_=(b&&(j==B[x-1])),c_=(c&&(l==C[x-1]));
dfs(a_,b_,c_,x-1);
f[a][b][c][x]+=f[a_][b_][c_][x-1];
f[a][b][c][x]%=p;
g[a][b][c][x]+=(g[a_][b_][c_][x-1]+f[a_][b_][c_][x-1]*l%p*inv[x-1]%p)%p;
g[a][b][c][x]%=p;
}
}
return;
}
inline void init(){
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
f[0][0][0][0]=1;
inv[0]=1;
for(int i=1;i<=lim;i++)inv[i]=(inv[i-1]<<1)%p;
return;
}
signed main(){
scanf("%lld",&T);
while(T--){
scanf("%lld%lld%lld%lld",&n,&m,&k,&p);
init();
for(int i=lim-1;i>=0;i--){
A[i]=(n>>i)&1;
B[i]=(m>>i)&1;
C[i]=(k>>i)&1;
}
dfs(1,1,1,lim);
printf("%lld\n",(g[1][1][1][lim]%p-k%p*f[1][1][1][lim]%p+p)%p);
}
return 0;
}
本文作者:trsins
本文链接:https://www.cnblogs.com/trsins/p/15815368.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步