2020杭电多校训练(第三、四场)

第三场

1005.Little-W-and-Contest

算法标签:并查集、组合数

因为可行的组合只有2、2、1和2、2、2两种
那么每次合并时统计两个集合中1和2的个数,并计算出“因这次合并而减少的组合”数量即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=1e5+5,MOD=1e9+7;

int T,n,m,p,x;

int FA[N],sz[N][2];

int getFa(int x)
{
    if (FA[x]==x) return x;
    else return FA[x]=getFa(FA[x]);
}
int cnt[2],u,v;
int inv[N];

int qpower(int x,int p)
{
    int ret=1;
    for (int base=x;p;p>>=1,base=(1ll*base*base)%MOD)
        if (p&1==1) ret=(1ll*ret*base)%MOD;
    return ret;
}
int Com(int n,int m)
{
    int ret=1;
    for (int i=1;i<=m;i++)
    {
        ret=(1ll*ret*(n-i+1))%MOD;
        ret=(1ll*ret*inv[i])%MOD;
    }
    return ret;
}
int main()
{
    for (int i=1;i<=N-1;i++) inv[i]=qpower(i,MOD-2);
    cin>>T;
    while(T--)
    {
        cin>>n;
        cnt[0]=cnt[1]=0;
        for (int i=1;i<=n;i++)
        {
            scanf("%d",&x);
            FA[i]=i;
            sz[i][0]=sz[i][1]=0;
            sz[i][x-1]=1;
            cnt[x-1]++;
        }
        ll ans=(Com(cnt[1],3)+(1ll*Com(cnt[1],2)*cnt[0])%MOD)%MOD;
        printf("%lld\n",ans);
        for (int i=1;i<=n-1;i++)
        {
            scanf("%d%d",&u,&v);
            u=getFa(u),v=getFa(v);
            int l0=cnt[0]-sz[u][0]-sz[v][0];
            int l1=cnt[1]-sz[u][1]-sz[v][1];
            ans=(ans-(((1ll*sz[u][1]*sz[v][1])%MOD)*(cnt[1]+cnt[0]-sz[u][1]-sz[u][0]-sz[v][1]-sz[v][0]))%MOD+MOD)%MOD;
            ans=(ans-(((1ll*sz[u][1]*sz[v][0]+1ll*sz[u][0]*sz[v][1])%MOD)*(cnt[1]-sz[u][1]-sz[v][1]))%MOD+MOD)%MOD;
            printf("%lld\n",ans);
            FA[v]=u;
            sz[u][0]+=sz[v][0];
            sz[u][1]+=sz[v][1];
        }
    }
    return 0;
}

1007.Tokitsukaze-and-Rescue

标签:最短路、随机数据

这题需要对随机数据比较敏感,因为给出的是完全图,边值又是随机的,所以从1到n的最短路所经过的边数的期望值一定很小,而k又是<=5的,所以直接跑最短路+暴力删去最短路上的边就能过了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=50+5;

const int INF=0x3f3f3f3f;

struct Edge{
    int from,to,val,nxt;
}e[2500];

int head[N];

int n,m,s,edgeN,ans;

int dis[N],pre[N];

vector<int>path[8];

int T,u,v,w,k;

void addEdge(int u,int v,int w)
{
    edgeN++;
    e[edgeN].from=u;
    e[edgeN].to=v;
    e[edgeN].val=w;
    e[edgeN].nxt=head[u];
    head[u]=edgeN;
}

struct Node{
    int dis,key;
    friend bool operator <(const Node &x,const Node &y)
    {
        return x.dis>y.dis;
    }
};

void dijkstra(int x)
{
    priority_queue<Node>Q;
    bool vis[N]={0};
    for (int i=1;i<=n;i++) dis[i]=INF;
    dis[1]=0;
    pre[1]=-1;
    Q.push({0,1});
    while(!Q.empty())
    {
        Node top=Q.top();
        Q.pop();
        int now=top.key;
        if (vis[now]) continue;
        vis[now]=1;
        for (int i=head[now];~i;i=e[i].nxt)
        {
            int to=e[i].to;
            if (dis[to]>dis[now]+e[i].val)
            {
                pre[to]=i;
                dis[to]=dis[now]+e[i].val;
                if (!vis[to]) Q.push({dis[to],to});
            }
        }
    }
    if (x!=0)
    {
        int now=n;
        while(pre[now]!=-1)
        {
            path[x].push_back(pre[now]);
            now=e[pre[now]].from;
        }
    }
}

void dfs(int l)
{
    if (l==0)
    {
        dijkstra(l);
        ans=max(ans,dis[n]);
        return;
    }
    path[l].clear();
    dijkstra(l);
    for (auto x:path[l])
    {
        int sv=e[x].val;
        e[x].val=INF;
        e[x^1].val=INF;
        dfs(l-1);
        e[x].val=sv;
        e[x^1].val=sv;
    }
}
void init()
{
    ans=0;
    memset(head,-1,sizeof(head));
    edgeN=-1;
}
void solve()
{
    cin>>n>>k;
    init();
    for (int i=1;i<=n*(n-1)/2;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        addEdge(u,v,w);addEdge(v,u,w);
    }
    dfs(k);
    printf("%d\n",ans);
}

int main()
{
    cin>>T;
    while(T--) solve();
    return 0;
}

1008.Triangle-Collision

标签:思维,点和向量旋转后的坐标变换

把小球在反射视作“穿入了镜子中的世界”是最关键的思考点。于是小球不再发生反射,而是产生了无数个边相邻的三角形。
接着考虑二分答案,计算出给定时间内小球穿过的边的个数。
实际上只要计算出小球穿过的平行于x轴的那条边的个数,然后把小球的位置和速度绕着三角形重心旋转两次,分别计算即可。
比较有价值的是点和向量旋转后的坐标变换公式。

假设对图片上任意点(x,y),绕一个坐标点(rx0,ry0)逆时针旋转a角度后的新的坐标设为(x0, y0),有公式:
x0=(x-rx0)*cos(a)-(y-ry0)*sin(a)+rx0;
y0=(x-rx0)*sin(a)+(y-ry0)*cos(a)+ry0;
向量(x,y)逆时针旋转a角度后新的向量为(x0,y0),有公式:
x0=x*cos(a)-y*sin(a);
y0=x*sin(a)+y*cos(a);

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=1e5+5;
const double eps=1e-6,pi=4.0*atan(1.0);

int T,k;

double L,X,Y,VX,VY,x,y,vx,vy,H;

void Rotate(double &x,double &y,double &vx,double &vy,double x0,double y0,double a)
{
	double x_,vx_,y_,vy_;
	x_=(x-x0)*cos(a)-(y-y0)*sin(a)+x0;
	vx_=vx*cos(a)-vy*sin(a);
	y_=(x-x0)*sin(a)+(y-y0)*cos(a)+y0;
	vy_=vx*sin(a)+vy*cos(a);
	x=x_,y=y_,vx=vx_,vy=vy_;
}

double cal(double x,double y,double vx,double vy,double t)
{
	double len;
	if (vy<0) len=-t*vy-y;
	else len=t*vy-(H-y);
	if (len>=0) return floor(len/H)+1;
	else return 0;
}
ll check(double t)
{
	ll sum=0;
	double len;
	x=X,y=Y,vx=VX,vy=VY;
	sum+=cal(x,y,vx,vy,t);

	Rotate(x,y,vx,vy,0,H/3,2*pi/3);
	sum+=cal(x,y,vx,vy,t);

	x=X,y=Y,vx=VX,vy=VY;
	Rotate(x,y,vx,vy,0,H/3,4*pi/3);
	sum+=cal(x,y,vx,vy,t);
	return sum;
}

int main()
{
	//freopen("a.in","r",stdin);
	cin>>T;
	while(T--)
	{
		scanf("%lf%lf%lf%lf%lf%d",&L,&X,&Y,&VX,&VY,&k);
		H=sqrt(3)*L/2;
		x=X,y=Y,vx=VX,vy=VY;
		double l=0,r=1e9;
		check(2);
		while(r-l>eps)
		{
			double mid=(l+r)/2;
			if (check(mid)>=k) r=mid;
			else l=mid;
		}
		printf("%.8lf\n",(l+r)/2);
	}
	//fclose(stdin);
    return 0;
}

1006.X-Number

标签:数位dp、组合数

这题让我对数位dp有了一些新的理解。。如果某些状态能够通过数学方法快速计算,那么不一定要通过dp转移,而是直接计算出结果就可以了。

那么对于这道题,什么情况下可以直接计算呢?假设前k位的情况已经确定了,并且

  1. 第k+1位的取值范围没有限制(即数位dp中的limit为0)
  2. 最高位已经确定了(前k位不全为0)

那么就可以通过另一个dp+组合数的方法计算出合法的方案数了。具体看代码中的dp0。

对于除此之外的情况,因为不能直接计算,所以像正常的数位dp一样进行转移即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

map<array<int,10>,ll>dp[20][2];

array<int,10>status={0};

int T,d,digit[20];

ll l,r,cc[20][20],dp0[20];

ll C(int n,int m)
{
	ll res=1;
	if (m>n-m) m=n-m;
	if (~cc[n][m]) return cc[n][m];
	for (int i=1;i<=m;i++)
		res*=(n-m+i),res/=i;
	return cc[n][m]=res;
}

ll dfs(int pos,bool limit,bool f)
{
	if (!limit && dp[pos][f].find(status)!=dp[pos][f].end()) return dp[pos][f][status];
	if (pos==0)
	{
		for (int i=0;i<=9;i++)
		{
			if (i==d) continue;
			if (status[i]>=status[d]) return dp[pos][f][status]=0;
		}
		return dp[pos][f][status]=1;
	}
	int up=limit?digit[pos]:9;
	ll sum=0;
	if (!limit && f)
	{
		for (int i=0;i<=pos;i++)
		{
			for (int i0=0;i0<=pos-i;i0++) dp0[i0]=0;
			dp0[pos-i]=C(pos,i);
			for (int i0=0;i0<10;i0++)
			{
				if (i0==d) continue;
				if (status[i0]>=status[d]+i)
				{
					dp0[0]=0;
					break;
				}
				for (int i1=0;i1<=pos-i;i1++)
					for (int i2=1;i2<=min(i1,status[d]+i-status[i0]-1);i2++)
						dp0[i1-i2]+=C(i1,i2)*dp0[i1];
			}
			sum+=dp0[0];
		}
	}
	else
	{
		for (int i=0;i<=up;i++)
		{
			if (i||f)
			{
				status[i]++;
				sum+=dfs(pos-1,limit&&(i==up),1);
				status[i]--;
			}
			else sum+=dfs(pos-1,limit&&(i==up),0);
		}
	}
	return limit?sum:dp[pos][f][status]=sum;
}

ll div(ll tmp)
{
	int p=0;
	while(tmp) digit[++p]=tmp%10,tmp/=10;
	return dfs(p,1,0);
}

void solve()
{
	cin>>l>>r>>d;
	for (int i=0;i<20;i++) for (int j=0;j<2;j++) dp[i][j].clear();
	printf("%lld\n",div(r)-div(l-1));
}

int main()
{
	cin>>T;
	memset(cc,-1,sizeof(cc));
	while(T--) solve();
	return 0;
}

第四场

1004.Deliver-the-Cake

标签:思维、最短路
把所有M的点拆成L和R两个点,连边的时候如果两个点属性相同边值就为w,属性不同就设为w+x

一开始想直接在dijsktra板子上面改,改来改去发现自己晕了。最后老老实实重新写了个拆点连边的才过的。这个故事告诉我们能在建模上完成的操作就不要靠改板子来实现了。。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=5e5+5;
const ll INF=1e18;

struct Edge{
    ll to,val;
};

vector<Edge>G[N];

struct Node{
    ll dis;
    int key;
    friend bool operator <(const Node &x,const Node &y)
    {
        return x.dis>y.dis;
    }
};

int n,m,s,t,x;

ll dis[N];

bool vis[N];

void dijkstra()
{
    priority_queue<Node>Q;
    memset(vis,0,sizeof(vis));
    for (int i=1;i<=n*2;i++) dis[i]=INF;
    dis[s]=0;
    Q.push({0,s});
    while(!Q.empty())
    {
        Node top=Q.top();
        Q.pop();
        int now=top.key;
        if (vis[now]) continue;
        vis[now]=1;
        for (int i=0;i<G[now].size();i++)
        {
            int to=G[now][i].to;
            if (dis[to]>dis[now]+G[now][i].val)
            {
                dis[to]=dis[now]+G[now][i].val;
                if (!vis[to]) Q.push({dis[to],to});
            }
        }
    }
}
char ss[N];
int tag[N];
int T,u,v,w;

int main()
{
    cin>>T;
    while(T--)
    {
        cin>>n>>m>>s>>t>>x;
        scanf("%s",ss+1);
        for (int i=1;i<=n*2;i++) G[i].clear();
        for (int i=1;i<=n;i++)
        {
            if (ss[i]=='L') tag[i]=0;
            if (ss[i]=='R') tag[i]=1;
            if (ss[i]=='M') tag[i]=2;
        }
        for (int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            if (tag[u]!=2 && tag[v]!=2)
            {
                G[n*tag[u]+u].push_back({n*tag[v]+v,w+(tag[u]==tag[v]?0:x)});
                G[n*tag[v]+v].push_back({n*tag[u]+u,w+(tag[u]==tag[v]?0:x)});
            }
            if (tag[u]==2 && tag[v]!=2)
            {
                G[u].push_back({n*tag[v]+v,w+(tag[v]==0?0:x)});
                G[n+u].push_back({n*tag[v]+v,w+(tag[v]==1?0:x)});
                G[n*tag[v]+v].push_back({u,w+(tag[v]==0?0:x)});
                G[n*tag[v]+v].push_back({n+u,w+(tag[v]==1?0:x)});
            }
            if (tag[v]==2 && tag[u]!=2)
            {
                G[v].push_back({n*tag[u]+u,w+(tag[u]==0?0:x)});
                G[n+v].push_back({n*tag[u]+u,w+(tag[u]==1?0:x)});
                G[n*tag[u]+u].push_back({v,w+(tag[u]==0?0:x)});
                G[n*tag[u]+u].push_back({n+v,w+(tag[u]==1?0:x)});
            }
            if (tag[u]==2 && tag[v]==2)
            {
                G[u].push_back({v,w});
                G[u].push_back({n+v,w+x});
                G[v].push_back({u,w});
                G[v].push_back({n+u,w+x});
                G[n+u].push_back({v,w+x});
                G[n+u].push_back({n+v,w});
                G[n+v].push_back({u,w+x});
                G[n+v].push_back({n+u,w});
            }
        }
        if (tag[s]!=2)
        {
            s=s+tag[s]*n;
            dijkstra();
            if (tag[t]!=2)
            {
                printf("%lld\n",dis[t+tag[t]*n]);
            }
            else
            {
                printf("%lld\n",min(dis[t],dis[t+n]));
            }
        }
        else
        {
            ll ans=INF;
            s=s;
            dijkstra();
            if (tag[t]!=2)
            {
                ans=min(ans,dis[t+tag[t]*n]);
            }
            else
            {
                ans=min(ans,min(dis[t],dis[n+t]));
            }
            s=s+n;
            dijkstra();
            if (tag[t]!=2)
            {
                ans=min(ans,dis[t+tag[t]*n]);
            }
            else
            {
                ans=min(ans,min(dis[t],dis[n+t]));
            }
            printf("%lld\n",ans);
        }
    }
    return 0;
}

1007.Go-Running

标签:二分图最小点覆盖(最小点覆盖=最大匹配)

非常非常值得重点关注的一道题。。通过观察可以发现,所有x+t相同或者x-t相同的点都可以通过一次操作来覆盖。也就是可以将x+t和x-t视作一个物品的两个属性,一次操作可以消去某一属性相同的若干个物品,问最少需要多少次操作。

再把每个属性值视作一个点,把每个物品视作一条边,左右端点分别连在对应的属性点上,就转化成了一个二分图最小点覆盖的问题。

一个必须掌握(至少要记住)的结论是:二分图最小点覆盖=最大匹配

二分图最大匹配的König定理及其证明

(下一个专题会尝试写一下二分图、网络流、匹配问题相关的很多算法和定理)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=2e5+5;

int T,n,u,v;

ll s,t,dep[N];

int _cur[N];

bool used[N];

struct Edge{int to;ll v;int rev;};

vector<Edge>G[N];

void addEdge(int x,int y,ll v)
{
    G[x].push_back((Edge){y,v,(int)G[y].size()});
    G[y].push_back((Edge){x,0,(int)G[x].size()-1});
}
bool bfs()
{

    memset(used,0,sizeof(used));
    dep[s]=1,used[s]=1;
    queue<int>L;
    L.push(s);
    while(!L.empty())
    {
        int cur=L.front();
        L.pop();
        for (int i=0;i<G[cur].size();i++)
        {
            Edge &e=G[cur][i];
            if (!used[e.to] && e.v>0)
            {
                dep[e.to]=dep[cur]+1;
                used[e.to]=1;
                L.push(e.to);
            }
        }
    }
    if (used[t]) return true;
    else return false;
}
ll dfs(int c,ll flow)
{
    if (c==t) return flow;
    if (flow==0) return flow;
    ll ret=0,f;
    for (int &i=_cur[c];i<G[c].size();i++)
    {
        Edge &e=G[c][i];
        if (dep[e.to]==dep[c]+1 && (f=dfs(e.to,min(flow,e.v)))>0)
        {
            ret+=f;
            flow-=f;
            e.v-=f;
            G[e.to][e.rev].v+=f;
            if (flow==0) break;
        }
    }
    return ret;
}
ll Dinic()
{
    ll ans=0;
    while(bfs())
    {
        memset(_cur,0,sizeof(_cur));
        ans+=dfs(s,1ll*100*INT_MAX);
    }
    return ans;
}

void solve()
{
	cin>>n;
	map<int,int>sum,gap;
	int sum_cnt,gap_cnt;
	sum_cnt=gap_cnt=0;
	s=0,t=2*n+1;
	for (int i=0;i<=2*n+1;i++) G[i].clear();
	for (int i=1;i<=n;i++)
	{
		scanf("%d%d",&u,&v);
		if (sum[u+v]==0) sum[u+v]=++sum_cnt;
		if (gap[u-v]==0) gap[u-v]=++gap_cnt;
		addEdge(sum[u+v],n+gap[u-v],1);
	}
	for (auto x:sum) addEdge(s,x.second,1);
	for (auto x:gap) addEdge(n+x.second,t,1);
	printf("%d\n",Dinic());
}

int main()
{
	cin>>T;
	while(T--) solve();
	return 0;
}

1003.Contest-of-Rope-Pulling

标签:01背包,随机优化

把第二组的数视作(-wi,vi),要做的就是取出若干个数,使得 ∑ w \sum w w为0,且 ∑ v \sum v v尽可能大。很明显能考虑用01背包解决这个问题,但是由于时间复杂度为 O ( ( n + m ) 2 ⋅ m a x { w i } ) O((n+m)^2·max\{w_i\}) O((n+m)2max{wi}),好像又不太可做的样子。考虑到 ∑ w \sum w w最大取到 1 0 6 10^6 106,那么有没有办法把需要用考虑的上界降下来呢?答案是把n+m个数顺序随机打乱之后,我们只要考虑到上界为 l i m i t = n + m ⋅ m a x { w i } limit=\sqrt{n+m}·max\{w_i\} limit=n+m max{wi}的情况即可,因为超出这个范围的解概率是极低的,具体的概率大概是 1 0 − 7 10^{-7} 107,详见题解的证明。

涉及知识盲区的一道题,完全想不到通过随机来优化dp。。以后还是要多练一点随机处理的题目

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=2e3+5,INF=0x3f3f3f3f3f,M=2e6+5;

mt19937 rnd(time(NULL));

int T,n,m;

struct Node{
	int w,v,ty;
	ll seed;
};

Node A[N];

ll f[M],g[M];

ll query(int x)
{
	if (x>=0) return f[x];
	else return g[-x];
}

void modify(int x,ll v)
{
	if (x>=0) f[x]=v;
	else g[-x]=v;
}

void solve()
{
	cin>>n>>m;
	for (int i=1;i<=n+m;i++)
	{
		scanf("%d%d",&A[i].w,&A[i].v);
		A[i].ty=i<=n;
		A[i].seed=rnd();
	}
	sort(A+1,A+1+n+m,[&](Node a,Node b){return a.seed<b.seed;});
	int limit=sqrt(n+m)*1000;
	for (int i=0;i<=limit;i++) f[i]=g[i]=-INF;
	f[0]=0;
	for (int i=1;i<=n+m;i++)
	{
		if (A[i].ty==1)
		{
			for (int j=limit;j>=-limit;j--)
			{
				if (j-A[i].w<-limit) continue;
				ll ask=query(j-A[i].w);
				if (ask==-INF) continue;
				if (ask+A[i].v>query(j))
					modify(j,ask+A[i].v);
			}
		}
		else
		{
			for (int j=-limit;j<=limit;j++)
			{
				if (j+A[i].w>limit) continue;
				ll ask=query(j+A[i].w);
				if (ask==-INF) continue;
				if (ask+A[i].v>query(j))
					modify(j,ask+A[i].v);
			}
		}
	}
	printf("%lld\n",f[0]);
}

int main()
{
	cin>>T;
	while(T--) solve();
	return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值