【集训总结】CSP-S/NOIP 2022 清北学堂 集训总结

END OF OI —— CSP-S/NOIP 2022 清北学堂

Day 1 搜索进阶 模拟赛

埃及分数

迭代加深搜索,每次迭代加深时可以发现下一次的分母一定比上一次大,所以可以从pre(表示上一次的分母)+1开始枚举,然后又能发现用初始的\(\frac{a}{b}\)-每次枚举的\(\frac{1}{i}\)一定要大于等于下一次枚举的值,倒一下柿子可以进一步的减少搜索范围,实质就是迭代加深+剪枝

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=1010;

int a,b;

int limit=1;

int ans[maxn];

int now[maxn];

bool get_ans;

int gcd(int x,int y)
{
	if(!y) return x;
	return gcd(y,x%y);
}

void moon(int &x,int &y)
{
	int g=gcd(x,y);
	x/=g;
	y/=g;
	return ;
}

void dfs(int deep,int son,int mo,int pre)
{
	int a=son,b=mo;
	moon(a,b);
	if(deep==1)
	{
		if(a==1 && b>pre)
		{
			if(get_ans && b>=ans[1]) return ;
			now[1]=b; get_ans=true;
			for(int i=1;i<=limit;i++) ans[i]=now[i];
			return ;
		}
		return ;
	}
	
	for(int i=pre;i<=ceil((double)b*deep/a);i++)
	{
		now[deep]=i;
		dfs(deep-1,a*i-b,b*i,i);
	}
	
	return ;
}

signed main()
{
	scanf("%d%d",&a,&b);
	
	moon(a,b);
	
	while(!get_ans)
	{
		dfs(limit,a,b,1);
		limit++;
	}
	
	for(int i=limit-1;i>=1;i--)
	{
		cout<<ans[i]<<" ";
	}
	
	return 0;
}

Day 2 基础算法 数据结构

无名の题

小朋友们分糖果,必须遵循两个原则

1,每个小朋友最少要分到1个糖果

2,相邻的小朋友中,如果体重不同,则体重更高的小朋友必须分到更多的糖果

贪心的例题,但之前学校考过

就是因为从左到右扫会由i-1对i产生影响,从右到左扫会由i+1对i产生影响,因此分别按不同顺序扫一遍,统计贡献

AC code

点击查看代码
#include<bits/stdc++.h>
#define int long long

using namespace std;

const int maxn=1e6+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	
	while(ch<'0' || ch>'9')
	{
		if(ch=='-')
		{
			f=-1;
		}
		ch=getchar();
	}
	
	while(ch>='0' && ch<='9')
	{
		w=w*10+ch-'0';
		ch=getchar();
	}
	
	return w*f;
}

int n,ans;

int a[maxn];

int c[maxn];

signed main()
{
	n=read();
	
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
	}
	
	for(int i=1;i<=n;i++)
	{
		c[i]=1;
	}
	
	for(int i=2;i<=n;i++)
	{
		if(a[i]>a[i-1] && c[i]<=c[i-1])
		{
			c[i]=c[i-1]+1;
		}
	}
	for(int i=n-1;i>=1;i--)
	{
		if(a[i]>a[i+1] && c[i]<=c[i+1])
		{
			c[i]=c[i+1]+1;
		}
	}
	
	for(int i=1;i<=n;i++)
	{
		ans+=c[i];
	}
	
	cout<<ans;
	
	return 0;
}

【HEOI 2016】排序

很巧妙的一道题,数据很水导致有些人暴力水过去了

将所有操作离线下来,因为要找第q位的数字,不难想到二分查找

我们令区间内比自己小的数为0,其他数为1,然后用线段树维护,排序时就按照0/1来排(比自己小的数的数量可以用线段树query,比较操作在build时就能完成)

然后看一下当前check的数字上的值是不是1,如果是1就说明慢足,否则return false

然后就是二分查找的板子

每次建树时要记得将lazy_tag初始化

主要考思维,代码难度不高

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>

using namespace std;

const int maxn=1e6+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int ans;

int n,m,q;

int a[maxn];

struct que
{
	int l,r,opt;
}qu[maxn];

struct s_t
{
	int l;
	int r;
	int val;
	int tag=-1;;
}t[maxn*4];

void build(int p,int l,int r,int x)
{
	t[p].l=l;
	t[p].r=r;
	if(l==r)
	{
		t[p].tag=-1;
		t[p].val=(a[l]>=x ? 1 : 0);
		return ;
	}
	int mid=(l+r)>>1;
	build(p*2,l,mid,x);
	build(p*2+1,mid+1,r,x);
	t[p].tag=-1;
	t[p].val=t[p*2].val+t[p*2+1].val;
}

void push_down(int p)
{
	if(t[p].tag>=0)
	{
		t[p*2].val=(t[p*2].r-t[p*2].l+1)*t[p].tag;
		t[p*2+1].val=(t[p*2+1].r-t[p*2+1].l+1)*t[p].tag;
		t[p*2].tag=t[p].tag;
		t[p*2+1].tag=t[p].tag;
		t[p].tag=-1;
	}
}

void update(int p,int l,int r,int k)
{
	if(l<=t[p].l && t[p].r<=r)
	{
		t[p].val=k*(t[p].r-t[p].l+1);
		t[p].tag=k;
		return ;
	}
	push_down(p);
	int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid) update(p*2,l,r,k);
	if(r>mid) update(p*2+1,l,r,k);
	t[p].val=t[p*2].val+t[p*2+1].val;
}

int query(int p,int l,int r)
{
	if(l<=t[p].l && t[p].r<=r)
	{
		return t[p].val;
	}
	push_down(p);
	int mid=(t[p].l+t[p].r)>>1;
	int ans=0;
	if(l<=mid)
	{
		ans+=query(p*2,l,r);
	}
	if(r>mid)
	{
		ans+=query(p*2+1,l,r);
	}
	return ans;
}

int query_point(int p,int k)
{
	if(t[p].l==t[p].r)
	{
		return t[p].val;
	}
	push_down(p);
	int mid=(t[p].l+t[p].r)>>1;
	int ans=0;
	if(k<=mid) { return query_point(p*2,k); }
	else { query_point(p*2+1,k); }
}

bool check(int x)
{
	build(1,1,n,x);
	for(int i=1;i<=m;i++)
	{
		int cnt=query(1,qu[i].l,qu[i].r);
		if(qu[i].opt==0)
		{
			update(1,qu[i].r-cnt+1,qu[i].r,1);
			update(1,qu[i].l,qu[i].r-cnt,0);
		}
		else
		{
			update(1,qu[i].l,qu[i].l+cnt-1,1);
			update(1,qu[i].l+cnt,qu[i].r,0);
		}
	}
	return query_point(1,q);
}

int main()
{
	n=read();
	
	m=read();
	
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
	}
	
	for(int i=1;i<=m;i++)
	{
		qu[i].opt=read();
		qu[i].l=read();
		qu[i].r=read();
	}
	
	q=read();
	
	int l=1,r=n;
	
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid))
		{
			l=mid+1;
			ans=mid;
		}
		else
		{
			r=mid-1;
		}
	}
	
	cout<<ans;
	
	return 0;
}

【POI 2011】PAT-Sticks

数据结构题(也是贪心),首先考虑颜色全都不同的操作,那么我们可以从大到小进行排序,然后每次找到三个相邻的数,判断能不能构成三角形即可

那么如果有好多种颜色,那么我们可以将每个颜色都搞一个大根堆,然后再搞一个最终存ans的PQ,每次可以选所有颜色里最长的那根(堆顶)push进PQ,然后每次取出三个最长的,如果符合直接输出,否则将最长的那根彻底放弃,再选一根同样颜色的最长棍push进PQ来,这样就可以保证颜色互不相同,问题就转化为了所有颜色都不同的情况

AC code

点击查看代码
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstdlib>
#define pii pair<int,int>

using namespace std;

const int maxn=1e6+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

struct node
{
	int col,len;
};

priority_queue <int> q[55];

priority_queue <pii> q_ans;

int k;

int main()
{
	k=read();
	
	for(int i=1;i<=k;i++)
	{
		int n=read();
		for(int j=1;j<=n;j++)
		{
			int len=read();
			q[i].push(len);
		}
	}

	for(int i=1;i<=k;i++)
	{
		if(!q[i].empty())
		{
			int val=q[i].top();
			q[i].pop();
			q_ans.push(make_pair(val,i));
		}
	}

	while(q_ans.size()>=3)
	{
		pair<int,int> a=q_ans.top(); q_ans.pop();
		pair<int,int> b=q_ans.top(); q_ans.pop();
		pair<int,int> c=q_ans.top(); q_ans.pop();
		
		if(c.first>a.first-b.first)
		{
			cout<<a.second<<" "<<a.first<<" ";
			cout<<b.second<<" "<<b.first<<" ";
			cout<<c.second<<" "<<c.first<<" ";
			exit(0);
		}
		else
		{
			q_ans.push(make_pair(b.first,b.second));
			q_ans.push(make_pair(c.first,c.second));
			
			if(!q[a.second].empty())
			{
				int k=q[a.second].top();
				q[a.second].pop();
				q_ans.push(make_pair(k,a.second));
			}
		}
	}
	
	cout<<"NIE";
	
	return 0;
}

【十二省联考 2019】春节十二响

看到题目就想起了流浪地球

可以先看部分分,有一种情况为链,可以启发我们

为一条链有两种情况

1.根节点为链的一端,那答案就是\(\sum {val_i}\)
2.根节点不为链的一端,那就可以每次找到左右两棵子树中val最大的节点,可以将其放在一个内存段,对答案的贡献就是两个节点者中的max(其实也是一种贪心策略),最后只剩一边有节点时就变成了情况1,统计ans

向普遍的情况进行扩展,我们可以将子树进行合并,完全朴素的合并可以得60pts

考虑像链的情况二一样进行合并,将子树化为堆,每次将siz小的堆合并到siz大的堆里,可以降低复杂度

即启发式合并

然后就像情况2那样统计答案即可

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<queue>
#include<vector>
#define int long long

using namespace std;

const int maxn=2e5+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,tot,ans;

int val[maxn];

int head[maxn];

vector <int> o;

priority_queue <int> q[maxn];

struct edge
{
	int to;
	int next;
}e[maxn*2];

void add(int x,int y)
{
	tot++;
	e[tot].to=y;
	e[tot].next=head[x];
	head[x]=tot;
}

void merge(int x,int y)
{
	if(q[x].size()<q[y].size()) swap(q[x],q[y]);
	
	while(!q[y].empty())
	{
		o.push_back(max(q[x].top(),q[y].top()));
		q[x].pop(),q[y].pop();
	}
	while(!o.empty())
	{
		q[x].push(o.back());
		o.pop_back();
	}
}

void dfs(int x)
{
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		dfs(to);
		merge(x,to);
	}
	q[x].push(val[x]);
}

signed main()
{
	n=read();
	
	for(int i=1;i<=n;i++) val[i]=read();
	
	for(int i=2;i<=n;i++)
	{
		int fa=read();
		add(fa,i);
	}
	
	dfs(1);
	
	while(!q[1].empty())
	{
		ans+=q[1].top();
		q[1].pop();
	}
	
	cout<<ans;
	
	return 0;
}

Day 3 数据结构 动态规划

Treeland Tour

数据范围6000,时限5s,(我第一次自己看出来)明显\(O(n^2log(n))\)的时间复杂度

又因为是LIS,不难想到nlog(n)是单调栈优化的序列上LIS的时间复杂度,此处将序列枚举的O(n)变为了遍历(dfs)树的时间复杂度,也就是说O(nlogn)用来转移,又因为根是不固定的,可以用O(n)枚举根,那么整体时间复杂度就是\(O(n^2logn)\)

相对较简单,就是把单调栈优化LIS板子搬到了树上

感觉洛谷难度评分过高

AC code

点击查看代码
#include<bits/stdc++.h>

using namespace std;

const int maxn=6010;

const int INF=INT_MAX;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,tot;

int ans=-INF;

int a[maxn];

int head[maxn];

int stac[maxn];

struct edge
{
	int to;
	int next;
}e[maxn*2];

void add(int x,int y)
{
	tot++;
	e[tot].to=y;
	e[tot].next=head[x];
	head[x]=tot;
}

void dfs(int x,int fa)
{
	int k=lower_bound(stac,stac+n,a[x])-stac;
	ans=max(ans,k+1);
	int temp=stac[k];
	stac[k]=a[x];
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dfs(to,x);
	}
	stac[k]=temp;
}

int main()
{
	n=read();
	
	for(int i=1;i<=n;i++) a[i]=read();
	
	for(int i=1;i<n;i++)
	{
		int u=read();
		int v=read();
		add(u,v);
		add(v,u);
	}
	
	for(int i=0;i<=n;i++)
	{
		stac[i]=INF;
	}
	
	for(int i=1;i<=n;i++)
	{
		dfs(i,0);
	}
	
	cout<<ans;
	
	return 0;
}

楼房重建

lxl之前讲过的一道题目,问题可以抽象一下,在坐标系上画出来,把楼顶和原点连接起来,形成许多条正比例函数的直线,我们可以对直线求斜率,不难发现前面一个楼的斜率大于后面一栋楼,那后面的就看不见了

这样一来就成了zhx讲的离线问题,给出序列,问多少对数对满足\(i< j 且 a_i<a_j\)

问题就简单了很多,线段树分情况维护即可

顺便%%%%%两位cdqz爷

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<string>
#include<algorithm>

using namespace std;

const int maxn=100010;

int n,m;

struct s_t
{
	double max_k;
	int len; 
}t[4*maxn];

double k[maxn];

int get_len(int p,int l,int r,double rul)
{
	if(t[p].max_k<=rul)
	{
		return 0;
	}
	
	if(k[l]>rul)
	{
		return t[p].len;
	}
	
	if(l==r)
	{
		if(k[l]>rul)
		{
			return 1;		
		}
		else
		{
			return 0;
		}
	}
	
	int mid=(l+r)>>1;
		
	if(t[2*p].max_k<=rul)
	{
		return get_len(p*2+1,mid+1,r,rul);
	}
	
	if(t[p*2].max_k>rul)
	{
		return get_len(p*2,l,mid,rul)+t[p].len-t[p*2].len;
	} 
	
}

void change(int p,int l,int r,int x,int y)
{
	if(l==x && r==x)
	{
		t[p].max_k=(double)y/x;
		t[p].len=1;
		return ; 
	}
	
	int mid=(l+r)>>1;
	
	if(x<=mid)
	{
		change(p*2,l,mid,x,y);
	}
	
	if(x>mid)
	{
		change(p*2+1,mid+1,r,x,y);
	}
	
	t[p].max_k=max(t[p*2].max_k,t[p*2+1].max_k);
	
	t[p].len=t[p*2].len+get_len(p*2+1,mid+1,r,t[p*2].max_k);
}

int main()
{
	scanf("%d%d",&n,&m);
	
	for(int i=1;i<=m;i++)
	{
		int x,y;
		
		scanf("%d%d",&x,&y);
		
		k[x]=(double)y/x;
		
		change(1,1,n,x,y);
		
		printf("%d\n",t[1].len);
		
	}
	
	return 0;
}

DZY Loves Fibonacci Numbers

傻逼数据结构 就是线段树维护区间加,区间查询,但是不同的就是加上的是斐波那契数列

当时想的思路和C_liar大佬一样,维护前缀和,然后把l给放进标记里

然后被zhx否决了

大概因为一个值可以被修改多次,标记需要合并,但我们的做法明显只是修改了一次

两个斐波那契数列相加还是一个广义斐波那契,而可以发现广义斐波那契有一个性质

设首项为a,第二项为b,第i项可以表示为\(a\times x+b\times y\)

那么我们可以用f[i][0]表示x,f[i][1]表示y,可以用斐波那契的递推式预处理

然后分别预处理前缀和sum[i][0]和sum[i][1]

前i项的和就是\(a\times sum[i][0]+b\times sum[i][1]\)

我们将f和sum数组预处理出来,然后标记把a,b维护进去即可

代码真jb难调

不过确实,对线段树的理解更深了些

AC code

点击查看代码
#include<bits/stdc++.h>
#define int long long

using namespace std;

const int maxn=3e5+5;

const int mod=1e9+9;

inline int read()
{
    int w=0,f=1;
    char ch=getchar();
    while(ch<'0' || ch>'9')
    {
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9')
    {
        w=(w<<3)+(w<<1)+(ch^48);
        ch=getchar();
    }
    return w*f;
}

int n,m;

int a[maxn];

int f[maxn][2];

int sum[maxn][2];

struct s_t
{
    int l,r;
    int val;
    int a,b;
}t[maxn*4];

void build(int p,int l,int r)
{
    t[p].l=l,t[p].r=r;
    if(l==r)
    {
        t[p].val=a[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(p*2,l,mid);
    build(p*2+1,mid+1,r);
    t[p].val=(t[p*2].val+t[p*2+1].val)%mod;
}

void push_down(int p,int l,int r)
{
    if(t[p].a || t[p].b)
    {
        int len=t[p*2].r-t[p*2].l+1;
        t[p*2].a=(t[p*2].a+t[p].a)%mod;t[p*2].b=(t[p*2].b+t[p].b)%mod;
        t[p*2].val=(t[p*2].val+t[p].a*sum[len][0]%mod+t[p].b*sum[len][1]%mod)%mod;
        t[p*2+1].a=(t[p*2+1].a+t[p].a*f[t[p*2+1].l-l+1][0]%mod+t[p].b*f[t[p*2+1].l-l+1][1]%mod)%mod;
        t[p*2+1].b=(t[p*2+1].b+t[p].a*f[t[p*2+1].l-l+2][0]%mod+t[p].b*f[t[p*2+1].l-l+2][1]%mod)%mod;
        t[p*2+1].val=(t[p*2+1].val+t[p].a*(sum[t[p*2+1].r-l+1][0]-sum[t[p*2+1].l-l][0]+mod)%mod+t[p].b*(sum[t[p*2+1].r-l+1][1]-sum[t[p*2+1].l-l][1]+mod)%mod)%mod;
        t[p].a=0;t[p].b=0;
    }
}

void update(int p,int l,int r)
{
    if(l<=t[p].l && t[p].r<=r)
    {
        t[p].a=(t[p].a+f[t[p].l-l+1][0]+f[t[p].l-l+1][1])%mod;
        t[p].b=(t[p].b+f[t[p].l-l+2][0]+f[t[p].l-l+2][1])%mod;
        t[p].val=(t[p].val+sum[t[p].r-l+1][0]-sum[t[p].l-l][0] + mod +sum[t[p].r-l+1][1] + mod -sum[t[p].l-l][1] + mod)%mod;
        return ;
    }
    push_down(p,t[p].l,t[p].r);
    int mid=(t[p].l+t[p].r)>>1;
    if(l<=mid) update(p*2,l,r);
    if(r>mid) update(p*2+1,l,r);
    t[p].val=(t[p*2].val+t[p*2+1].val)%mod;
}

int query(int p,int l,int r)
{
    if(l<=t[p].l && t[p].r<=r)
    {
        return t[p].val%mod;
    }
    push_down(p,t[p].l,t[p].r);
    int mid=(t[p].l+t[p].r)>>1;
    int ans=0;
    if(l<=mid) ans=(ans+query(p*2,l,r))%mod;
    if(r>mid) ans=(ans+query(p*2+1,l,r))%mod;
    return ans;
}

signed main()
{
    n=read();
    m=read();

    f[1][0]=1,f[1][1]=0;
    f[2][0]=0,f[2][1]=1;

    for(int i=3;i<=n;i++)
    {
        f[i][0]=(f[i-1][0]+f[i-2][0])%mod;
        f[i][1]=(f[i-1][1]+f[i-2][1])%mod;
    }

    for(int i=1;i<=n;i++)
    {
        sum[i][0]=(sum[i-1][0]+f[i][0])%mod;
        sum[i][1]=(sum[i-1][1]+f[i][1])%mod;
    }

    for(int i=1;i<=n;i++) a[i]=read();

    build(1,1,n);

    for(int i=1;i<=m;i++)
    {
        int opt=read();
        int l=read();
        int r=read();
        if(opt==1)
        {
            update(1,l,r);
        }
        else
        {
            cout<<(query(1,l,r)+mod)%mod<<endl;
        }
    }

    return 0;
}

Day 4 动态规划

收集邮票

期望概率DP,设f[i]表示已经有了i张邮票,还要买邮票数量的期望值

g[i]表示已经有了i张邮票,还要买邮票花的钱的期望值

不难想出f[n]=0,有\(\frac{i}{n}\)的概率是买到已有的邮票,\(\frac{n-i}{n}\)的概率是买到没有买过的邮票

\(f[i]=\frac{i}{n}\times f[i]+\frac{n-i}{n}\times f[i+1]+1\)

化简得

\(f[i]=\frac{n}{n-i}+f[i+1]\)

因为我们要用动态规划求解,要求无后效性,而题目是有后效性的

我们可以转化一下

初始为一块钱,后面比前面贵了一块

g[i]转移时就要加上f[i]、f[i+1]再加上涨的1块钱

\(g[i]=\frac{n-i}{n}\times (g[i+1]+f[i+1])+\frac{i}{n}\times (g[i]+f[i])+1\)

化简TM自己化,LaTeX太shabi了

AC code

点击查看代码
#include<bits/stdc++.h>

using namespace std;

const int maxn=1e4+5;

int n;

double f[maxn];

double g[maxn];

int main()
{
	cin>>n;
	for(int i=n-1;i>=0;i--)
	{
		f[i]=f[i+1]+(double)(1.0*n/(n-i)*1.0);
		g[i]= g[i+1]+f[i+1]+(double)(1.0*i/(1.0*(n-i)))*f[i]+(double)(1.0*n/((n-i)*1.0));
	}
	
	printf("%.2lf",g[0]);
	
	return 0;
}

【SDOI 2006】保安站岗

一眼树形DP(都有图了还看不出来?)

每个点只有三种状态

1.自己有保安
2.儿子有保安
3.父亲有保安

dp[x][0/1/2]对应上边的3种状态,则显然

\(dp[x][0]=val[i]+\sum min(dp[to][0],dp[to][1],dp[to][2])\)

dp[x][1]有点复杂,先说dp[x][2]

\(dp[x][2]=\sum min(dp[to][0],dp[to][1])\)

\(dp[x][1]=\sum min(dp[to][0],dp[to][1])+dp[to'][0]\)

意思就是有一个儿子必须配保安

都挺好理解的,挺水的一个题

AC code

点击查看代码
#include<bits/stdc++.h>

using namespace std;

const int maxn=1510;

const int INF=INT_MAX;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,tot;

int val[maxn];

int head[maxn];

struct edge
{
	int to,next;
}e[maxn*2];

int dp[maxn][4];

void add(int x,int y)
{
	tot++;
	e[tot].to=y;
	e[tot].next=head[x];
	head[x]=tot;
}

void dfs(int x,int fa)
{
	dp[x][0]=val[x];
	int sum=0;
	dp[x][1]=INF;
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dfs(to,x);
		sum+=min(dp[to][0],dp[to][1]);
		dp[x][0]+=min(dp[to][0],min(dp[to][1],dp[to][2]));
		dp[x][2]+=min(dp[to][1],dp[to][0]);
	}
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dp[x][1]=min(dp[x][1],sum-min(dp[to][0],dp[to][1])+dp[to][0]);
	}
}

int main()
{
	n=read();
	
	for(int i=1;i<=n;i++) 
	{
		int id=read();
		val[id]=read();
		int m=read();
		for(int i=1;i<=m;i++)
		{
			int son=read();
			add(id,son);
			add(son,id);
		}
	}
	
	dfs(1,0);
	
	int ans=min(dp[1][0],dp[1][1]);
	
	cout<<ans;
	
	return 0;
}

【NOI 2005】聪聪与可可

期望概率经典题,详见这里

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<string>
#include<algorithm>
#include<queue> 

using namespace std;

const int maxn=1010;

const double inf=-1.0;

int tot;

double ans;

int n,e_num,c,m;

int ind[maxn];

int head[maxn];

int dis[maxn][maxn];

int nex[maxn][maxn];

double dp[maxn][maxn];

struct node
{
	int to;
	int next;
}e[2*maxn];

void add(int x,int y)
{
	tot++;
	e[tot].to=y;
	e[tot].next=head[x];
	head[x]=tot; 
}

void BFS(int x)
{
	queue <int> q;
	q.push(x);
	dis[x][x]=0;
	while(!q.empty())
	{
		int from=q.front();
		
		q.pop();
		
		for(int i=head[from];i;i=e[i].next)
		{
			int to=e[i].to;
			
			if(dis[x][to]==-1)
			{
				dis[x][to]=dis[x][from]+1;
				q.push(to);
			}
		}
	}
} 

double DFS(int x,int y)
{
	if(dp[x][y]!=-1.0)
	{
		return dp[x][y];
	}
	
	if(x==y)
	{
		return dp[x][y]=0;
	}
	
	if(nex[x][y]==y || nex[nex[x][y]][y]==y)
	{
		return dp[x][y]=1.0;
	}
	
	dp[x][y]=0.0;
	
	for(int i=head[y];i;i=e[i].next)
	{
		dp[x][y]+=DFS(nex[nex[x][y]][y],e[i].to);
	}
	dp[x][y]=(dp[x][y]+DFS(nex[nex[x][y]][y],y))/(double)(ind[y]+1)+1;
	
	return dp[x][y];
}

int main()
{
	cin>>n>>e_num;
	
	cin>>c>>m;
	
	for(int i=1;i<=e_num;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,y);
		add(y,x);
		ind[x]++;
		ind[y]++;
	}
	
	for(int i=0;i<=n;i++)
	{
		for(int j=0;j<=n;j++)
		{
			dp[i][j]=-1.0;
		}
	}
	
	memset(dis,-1,sizeof(dis));
	
	for(int i=1;i<=n;i++)
	{
		BFS(i);
	}
	
	memset(nex,0x3f3f3f,sizeof(nex));
	
	for(int i=1;i<=n;i++)
	{
		for(int j=head[i];j;j=e[j].next)
		{
			int to=e[j].to;
			for(int k=1;k<=n;k++)
			{
				if(dis[i][k]==dis[to][k]+1 && nex[i][k]>to)
				{
					nex[i][k]=to;
				}
			}
		}
	}
	
	ans=DFS(c,m);
	
	printf("%.3lf",ans);
	
	return 0;
} 

【AHOI 2009】中国象棋

怎么说呢,挺水的这个题

乍一看就是个状压DP,事实上有50%是状压

但是一看就是误导类部分分

实际上是区间DP

设dp[i][j][k]表示当前第i行,有j列放了1个棋子,k列放了2个棋子

然后就是暴力转移+分类讨论

也不知道为啥是蓝题

AC code

点击查看代码
#include<bits/stdc++.h>
#define int long long

using namespace std;

const int maxn=110;

const int mod=9999973;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,m;

int dp[maxn][maxn][maxn];

int c(int x)
{
	return ((x*(x-1))/2)%mod;
}

signed main()
{
	n=read();
	m=read();
	
	dp[0][0][0]=1;
	
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			for(int k=0;k+j<=m;k++)
			{
				dp[i][j][k]+=dp[i-1][j][k];
				if(k>=1) dp[i][j][k]+=dp[i-1][j+1][k-1]*(j+1);
				if(j>=1) dp[i][j][k]+=dp[i-1][j-1][k]*(m-(j-1)-k);
				if(k>=1) dp[i][j][k]+=dp[i-1][j][k-1]*j*(m-j-(k-1));
				if(j>=2) dp[i][j][k]+=dp[i-1][j-2][k]*c(m-(j-2)-k);
				if(k>=2) dp[i][j][k]+=dp[i-1][j+2][k-2]*c(j+2);
				dp[i][j][k]%=mod;
			}
		}
	}
	
	int ans=0;
	
	for(int i=0;i<=m;i++)
	{
		for(int j=0;j<=m;j++)
		{
			ans=(ans+dp[n][i][j])%mod;
		}
	}
	
	cout<<ans;
	
	return 0;
}

【SCOI 2009】奖励关

读题就是概率与期望DP,再看数据范围,明显状压DP

所以这道题是概率期望+状态压缩DP

设dp[i][s]表示第i轮,状态为s,我们显而易见的发现它无法正推,因为正推可能无法到达状态s,所以我们考虑倒推

用state数组将每个限制集合提前预处理出来

转移时就只有两种情况,即能选和不能选

而能选又可以分为选与不选

可以枚举状态,判断能不能选

能选则\(dp[i][j]+=max(dp[i+1][j],dp[i+1][j|(1<<k-1)]+p[k])\)

不能选则\(dp[i][j]+=dp[i+1][j]\)

最后再除n表示期望,挺简单的

AC code

点击查看代码
#include<bits/stdc++.h>

using namespace std;

const int maxm=110;

const int maxn=1<<15;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,k;

int p[20],sta[20];

double dp[maxm][maxn];

int main()
{
	k=read();
	n=read();
	
	for(int i=1;i<=n;i++)
	{
		int x;
		p[i]=read();
		while(x=read()) sta[i]=sta[i]|(1<<x-1);
	}
	
	for(int i=k;i>=1;i--)
	{
		for(int j=0;j<(1<<n);j++)
		{
			for(int l=1;l<=n;l++)
			{
				if((sta[l] & j)==sta[l])
				{
					dp[i][j]+=max(dp[i+1][j],dp[i+1][j|(1<<l-1)]+p[l]);					
				}
				else
				{
					dp[i][j]+=dp[i+1][j];
				}
			}
			dp[i][j]/=n;
		}
	}

	printf("%.6lf",dp[1][0]);
	
	return 0;
}

Day 5 图论

Cheap Robot

zhx找题果然bt,调了2h。。。。。

我们可以变换一下,首先考虑机器人在某个点离它最近的充电站到它的距离,我们将其设置为dis[i],如果对每个点都跑一边最短路,那我就不用干别的了 一个最短路就Game Over了

我们引入超级源点(这个之前涉及过,在差分约束),看自己喜好,这里我将其设为0号点,那么将0号点与其他所有能充电的点连边的边权全部设为0,由于是无向边,则不难想出其他不能充电的点到最近的能充电的点的距离为dis[i],即超级源点到他的最短路,用Dij预处理即可

然后我们会发现,设在这个点的电量为x,那x应该大于等于dis[i],小于等于电池容量c-dis[i](这个上课我口胡对力),所以\(dis_i \leq x \leq c-dis_i\),又因为从i->j,边权为val,那么在i剩余电量为x,则在j剩余电量为x-val,则有

\(dis_j \leq x-val \leq c-dis_j\)

\(dis_j+val \leq x \leq c-dis_j+val\)

可得\(dis_j+val \leq x \leq c-dis_i\)

\(dis_j+val \leq c-dis_i\)

\(dis_i+dis_j+val \leq c\)

我们通过预处理出dis[i],那么我们就可以将边权转化为\(dis_i+dis_j+val\),以这个为新的边权,建出它的一棵最小生成树来

那么答案显然是在树上两点间的所有边里最大的边权值,即树上区间最值

由于这里最小生成树是并查集存储的,我们可以新建一棵树,来实现最小生成树

然后就是树上区间最值了,zhx讲的是倍增

但是倍增多麻烦。。。。我直接树剖套线段树

TM define int long long被卡常了,懒得改了,直接吸氧AC

自己注意一下就好

AC code

点击查看代码
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<cstdio>
#include<queue>
#define int long long
#define pii pair<int,int>

using namespace std;

const int maxm=1e5+5;

const int maxn=4e5+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int fath[maxn];

bool vis[maxn];

int max_son[maxn];

long long a[maxn];

long long dis[maxm];

long long val[maxn];

int head_tree[maxn];

int n,tot,m,k,q,cnt;

int top[maxn],id[maxn];

int head[maxn],fa[maxn];

int siz[maxn],deep[maxn];

struct edge
{
	int to;
	int next;
	long long val;
}e[maxn*2],tree[maxn*2];

struct s_t
{
	int l,r;
	int val;
}t[maxn*4];

struct node
{
	int from;
	int to;
	long long val;
}edg[maxn];

void add(int x,int y,int z)
{
	tot++;
	e[tot].to=y;
	e[tot].val=z;
	e[tot].next=head[x];
	head[x]=tot;
}

void add_tree(int x,int y,int z)
{
	tot++;
	tree[tot].to=y;
	tree[tot].val=z;
	tree[tot].next=head_tree[x];
	head_tree[x]=tot;
}

bool cmp(node x,node y)
{
	return x.val<y.val;
}

void init()
{
	n=read();
	m=read();
	k=read();
	q=read();
	for(int i=1;i<=m;i++)
	{
		int u=read();
		int v=read();
		long long val=read();
		add(u,v,val);
		add(v,u,val);
		edg[i]={u,v,val};
	}
}

void dij()
{
	for(int i=1;i<=k;i++) add(0,i,0);
	priority_queue <pii,vector<pii>,greater<pii> > q;
	memset(dis,0x3f,sizeof(dis));
	memset(vis,false,sizeof(vis));
	dis[0]=0; q.push(make_pair(dis[0],0));
	while(!q.empty())
	{
		int k=q.top().second;
		q.pop();
		if(vis[k]) continue;
		vis[k]=true;
		for(int i=head[k];i;i=e[i].next)
		{
			int to=e[i].to;
			if(vis[to]) continue;
			if(dis[to]>dis[k]+e[i].val)
			{
				dis[to]=dis[k]+e[i].val;
				q.push(make_pair(dis[to],to));
			}
		}
	}	
}

int find(int x)
{
	if(x==fa[x]) return x;
	return fa[x]=find(fa[x]);
}

void merger(int x,int y)
{
	fa[y]=x;
}

void kruskal()
{	
	for(int i=1;i<=m;i++)
	{
		int u=edg[i].from;
		int v=edg[i].to;
		edg[i].val+=dis[u]+dis[v];
	}
	for(int i=1;i<=n;i++) fa[i]=i;
	sort(edg+1,edg+m+1,cmp);tot=0;
	for(int i=1;i<=m;i++)
	{
		int u=edg[i].from;
		int v=edg[i].to;
		long long w=edg[i].val;
		if(find(u)==find(v)) continue;
		merger(find(u),find(v));cnt++;
		add_tree(u,v,w);add_tree(v,u,w);
		if(cnt==n-1) break;
	}
}

void dfs_first(int x,int f)
{
	siz[x]=1;deep[x]=deep[f]+1;fath[x]=f;
	for(int i=head_tree[x];i;i=tree[i].next)
	{
		int to=tree[i].to;
		if(to==f) continue;
		val[to]=tree[i].val;
		dfs_first(to,x);
		siz[x]+=siz[to];
		if(siz[to]>siz[max_son[x]])
		{
			max_son[x]=to;
		}
	}
}

void dfs_second(int x,int t)
{
	top[x]=t; tot++;
	id[x]=tot;
	a[tot]=val[x];
	if(max_son[x]==0) return ;
	dfs_second(max_son[x],top[x]);
	for(int i=head_tree[x];i;i=tree[i].next)
	{
		int to=tree[i].to;
		if(to!=max_son[x] && to!=fath[x])
			dfs_second(to,to);
	}
}

void build(int p,int l,int r)
{
	t[p].l=l,t[p].r=r;
	if(l==r)
	{
		t[p].val=a[l];
		return ;
	}
	int mid=(l+r)>>1;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	t[p].val=max(t[p*2].val,t[p*2+1].val);
}

long long query(int p,int l,int r)
{
	if(l>r) return 0;
	if(l<=t[p].l && t[p].r<=r)
	{
		return t[p].val;
	}
	int mid=(t[p].l+t[p].r)>>1;
	long long ans=0;
	if(l<=mid)
	{
		ans=max(ans,query(p*2,l,r));
	}
	if(r>mid)
	{
		ans=max(ans,query(p*2+1,l,r));
	}
	return ans;
}

long long get_ans(int x,int y)
{
	long long ans=0;
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]]) swap(x,y);
		ans=max(ans,query(1,id[top[x]],id[x]));
		x=fath[top[x]];
	}
	if(deep[x]>deep[y])	swap(x,y);
	ans=max(ans,query(1,id[x]+1,id[y]));
	return ans;
}

signed main()
{	
	init();dij();kruskal();
	
	tot=0; dfs_first(1,0);
	tot=0; dfs_second(1,1);
	
	build(1,1,n);
	
	for(int i=1;i<=q;i++)
	{
		int x=read();
		int y=read();
		cout<<get_ans(x,y)<<endl;
	}
	
	return 0;
}

白雪皑皑

这题不是图论。。。。原题叫疯狂的馒头,和这题一样的,在BZOJ上

这题就是并查集,因为zhx在讲kruskal,就讲了并查集(果然是一个人讲图论和数据结构)

很简单的,其实就是暴力。。。

对的,操作次数1e7,数据范围1e6的题,暴力是是正解

这也告诉我们:暴力出奇迹

但是盲目的暴力肯定不行,每片雪花最后的颜色肯定是取决于最后一次染色,所以我们倒着遍历,用并查集可以标记它是否被染过色,那我们可以做到每片雪花只被遍历一次,复杂度接近O(n),之所以是接近而不等于是因为并查集不完全是O(1)而是反Ackermann,但是基本上可以忽略

AC code

点击查看代码
#include<iostream>
#include<queue>
#include<cstdio>
#include<stack>
#include<vector>
#include<algorithm>
#include<string>
#include<cstring>
#include<cmath>
#include<climits>

using namespace std;

const int MAXN=1000010;

int n,m,p,q;

int color[MAXN];

int father[MAXN];

int find(int x)
{
	if(father[x]<0)
	{
		return x;
	}
	return father[x]=find(father[x]);
}

int main()
{
	cin>>n>>m>>p>>q;
	
	memset(father,-1,sizeof(father));
	
	for(int i=m;i>=1;i--)
	{
		int mi=(i*p+q)%n+1;
		int ma=(i*q+p)%n+1;
		
		if(mi>ma)
		{
			swap(mi,ma);
		}
		
		mi=find(mi);
		while(mi<=ma)
		{
			color[mi]=i;
			father[mi]=mi+1;
			mi=find(mi+1);
		}		
	}
	
	for(int i=1;i<=n;i++)
	{
		cout<<color[i]<<endl;
	}
	
	return 0;
}

Day 6 图论 数论

卢卡斯定理

Lucas定理 证明过程:

反之亦然同理,推论自然成立,略去过程QED,由上可知证毕

证明就这样结束了,说一下板子怎么写

就是在mod p的情况下求C[n][m],我们可以把n,m拆为p进制,让他俩对齐(位数不够的补零),然后对于第i位的C[n[i]][m[i]],\(ans=\prod c[n[i]][m[i]]\mod p\)

AC code

点击查看代码
#include<bits/stdc++.h>
#define int long long

using namespace std;

const int maxn=1e5+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int t,n,m,p;

int fac[maxn];

int cnt_n,cnt_m;

int p_n[maxn],p_m[maxn];

int qpow(int a,int b,int mod)
{
	int ans=1;
	for(;b;b>>=1,a=(a*a)%mod)
	{
		if(b&1) ans=(ans*a)%mod;
	}
	return ans;
}

int c(int n,int m)
{
	if(m>n) return 0;
	return fac[n]*(qpow(fac[m],p-2,p)%p)*(qpow(fac[n-m],p-2,p)%p);
}

int lucas(int n,int m)
{
	if(!m) return 1;
	return lucas(n/p,m/p)%p*c(n%p,m%p)%p;
}

signed main()
{
	t=read();
	while(t--)
	{
		n=read();m=read();
		p=read();fac[0]=1;
		for(int i=1;i<=p;i++)
		{
			fac[i]=(fac[i-1]*i)%p;
		}
		cout<<lucas(n+m,n)<<'\n';
	}
}

【APIO 2009】抢掠计划

APIO出板子题。。。。 每次抢劫进入一个强连通分量,则可以把里面的钱都抢完,然后让你求最多抢多少,即经过的最大点权

Tarjin+Topo_sort+DP 就好了

缩点时只需要对start进行Tarjan,然后如果起点无法到达的点直接continue掉,否则可能产生影响

AC code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int maxn=5e5+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int s,p,t;

int n,m,tot,ans;

int id[maxn];

stack <int> st;

bool vis[maxn];

int head[maxn];

int t_head[maxn];

int ind[maxn],dp[maxn];

int val[maxn],dfn[maxn];

int bel[maxn],low[maxn];

struct edge
{
	int to,next;
}e[maxn*2],topo_e[maxn*2];

void add(int x,int y)
{
	e[++tot].to=y;
	e[tot].next=head[x];
	head[x]=tot;
}

void add_e(int x,int y)
{
	topo_e[++tot].to=y;
	topo_e[tot].next=t_head[x];
	t_head[x]=tot;
}

void tarjan(int x)
{
	dfn[x]=low[x]=++t;
	st.push(x);vis[x]=true;
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(!dfn[to])
		{
			tarjan(to);
			low[x]=min(low[x],low[to]);
		}
		else if(vis[to])
		{
			low[x]=min(low[x],low[to]);
		}
	}
	if(dfn[x]==low[x])
	{
		while(st.top()!=x)
		{
			bel[st.top()]=x;
			vis[st.top()]=false;
			val[x]+=val[st.top()];
			st.pop();
		}
		st.pop();
		bel[x]=x;
		vis[x]=false;
	}
}

void topo_sort()
{
	queue <int> q;
	q.push(bel[s]);
	dp[bel[s]]=val[bel[s]];
	while(!q.empty())
	{
		int k=q.front();
		q.pop();
		for(int i=t_head[k];i;i=topo_e[i].next)
		{
			int to=topo_e[i].to;
			dp[to]=max(dp[to],dp[k]+val[to]);
			if(--ind[to]==0) q.push(to);
		}
	}
	for(int i=1;i<=n;i++)
	{
		if(id[i]) ans=max(ans,dp[bel[i]]);
	}
}

signed main()
{
	n=read();
	m=read();
	
	for(int i=1;i<=m;i++)
	{
		int u=read();
		int v=read();
		add(u,v);
	}
	
	for(int i=1;i<=n;i++) val[i]=read();
	
	s=read(),p=read();
	
	tarjan(s);
	
	for(int i=1;i<=p;i++)
	{
		int x=read();id[x]++;
	}
	
	tot=0;
	
	for(int i=1;i<=n;i++)
	{
		if(!bel[i]) continue;
		for(int j=head[i];j;j=e[j].next)
		{
			int to=e[j].to;
			if(bel[i]!=bel[to])
			{
				add_e(bel[i],bel[to]);
				ind[bel[to]]++;
			}
		}
	}
	
	topo_sort();
	
	cout<<ans;
	
	return 0;
}
posted @ 2022-10-02 18:14  NinT_W  阅读(58)  评论(0编辑  收藏  举报