zoukankan      html  css  js  c++  java
  • HNOI 20122013 题目泛做

    2012 集合选数

    题目描述

    点此看题

    解法

    要不是吃饭去了我肯定能完全想明白,话说网上的题解点都不负责任,构造怎么得来的不写一下?

    先考虑只有 \(2x\) 被禁用的情况,一开始我想了很多方法都避免不了状压,究其原因是限制过于分散造成我们需要记录的信息太多。回想限制最紧凑的模型是线性 \(dp\),因为它的限制都是挨在一起的所以无需记录。

    但是又注意到问题的限制是不交的链,那么对于每条链我们可以取出来分别计算,然后乘法原理合并。计算每条链的时候用线性 \(dp\) 就行了,要求只有相邻两个数不能都选。

    回到本题 \(2x,3x\) 的情况,那么问题的限制是若干个不交的矩形,要求是相邻的数不能同时选:

    1 2 4 8 .....
    3 6 12 24 ....
    9 18 36 72 ....
    ....
    

    因为矩形的大小很小,所以我们对行状压然后暴力转移即可,注意去掉不合法状态就可以轻松跑过。

    #include <cstdio>
    const int M = 100005;
    const int MOD = 1e9+1;
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,ans,vis[M],lim[M];
    int a[12][20],dp[12][1<<18],g[1<<18];
    void build(int x)
    {
    	for(int i=1;i<=11;i++)
    	{
    		if(i==1) a[1][1]=x;
    		else a[i][1]=a[i-1][1]*3;
    		if(a[i][1]>n) break;
    		m=i;vis[a[i][1]]=1;int cnt=1;
    		for(int j=2;j<=18;j++)
    		{
    			a[i][j]=a[i][j-1]*2;
    			if(a[i][j]>n) break;
    			vis[a[i][j]]=1;cnt=j;
    		}
    		lim[i]=1<<cnt;
    	}
    }
    int ask(int x)
    {
    	int res=0;
    	for(int i=0;i<lim[1];i++)
    		dp[1][i]=g[i];
    	for(int i=2;i<=m;i++) for(int j=0;j<lim[i];j++)
    	{
    		if(!g[j]) continue;dp[i][j]=0;
    		for(int k=0;k<lim[i-1];k++)
    			if(g[k] && (k&j)==0)
    				dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
    	}
    	for(int i=0;i<lim[m];i++)
    		res=(res+dp[m][i])%MOD;
    	return res;
    }
    signed main()
    {
    	n=read();ans=1;
    	for(int i=0;i<(1<<18);i++)
    		g[i]=!((i<<1)&(i));
    	for(int i=1;i<=n;i++) if(!vis[i])
    		build(i),ans=1ll*ans*ask(i)%MOD;
    	printf("%d\n",ans);
    }
    

    2012 与非

    题目描述

    点此看题

    解法

    我真他吗要困死了,真的要尊重生理规律啊,晚上睡晚了我现在想死 \(.....\)

    既然题目是问 \([L,R]\) 中能被凑出的数的个数,而且还是位运算,那么我们考虑魔改线性基。那么考虑线性基里面的第 \(i\) 个元素应该是,\(i\) 个数位是 \(1\),前面的数位都是 \(0\),后面的数位尽量为 \(0\)

    这样设计线性基的元素目的是可以很容易地消去\(/\)增添第 \(i\) 个数位的值,并且对较大的数位无影响,并且对较小的数位影响尽量小。并且我们可以得到一个性质:是如果线性基某数位 \(i\) 的元素在数位 \(j\) 上有值(\(i\not=j\)),那么线性基的数位 \(j\) 一定没有元素,这是因为如果有的话为了化到最简可以消去。

    那么如何构造出线性基呢?这里我们不采取依次插入的模式,而是从大到小依次构造。利用性质可以判断第 \(i\) 位是否应该有元素,如果有的话我们把每个都用上。设现在的数是 \(x\),那么我们把 \(x\) 和自己操作一次之后再和新加入的数 \(a\) 操作,操作的效果类似于取并集,这样我们就达到了简化当前元素的目的。

    最后考虑怎么计算答案,首先我们可以做一个差分,然后就需要用到紧贴的思想,如果上界在这一位上位 \(1\) 那么考虑如果这一位填 \(0\) 后面可以任意填,如果填 \(1\) 那么继续循环,时间复杂度 \(O(n\log n)\)

    更多的细节还是需要看看代码,难以讲得特别清楚。

    #include <cstdio>
    #define int long long
    const int M = 1005; 
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,l,r,a[M],b[M],sum[M];
    int ask(int x)
    {
    	if(x<0) return -1;
    	if(x>(1ll<<m)-1) return (1ll<<sum[m-1])-1;
    	int res=0,s=0;
    	for(int i=m-1;i>=0;i--) if(x>>i&1)
    	{
    		//0
    		if(i && !(s>>i&1))
    			res+=(1ll<<sum[i-1])-1;
    		//1
    		if(b[i])//Meanwhile s>>i&1==0
    		{
    			s|=b[i];
    			if(s>x) break;
    			res++;
    		}
    		else if(!(s>>i&1)) break;
    	}
    	return res;
    }
    signed main()
    {
    	n=read();m=read();l=read();r=read();
    	for(int i=1;i<=n;i++) a[i]=read();
    	for(int i=m-1,s=0;i>=0;i--)
    		if(!(s>>i&1))
    		{
    			int &now=b[i];now=(1ll<<m)-1;
    			for(int j=1;j<=n;j++)
    				if(a[j]>>i&1) now&=a[j];
    				else now&=~a[j];
    			sum[i]=1;s|=now;
    		}
    	for(int i=1;i<m;i++) sum[i]+=sum[i-1];
    	printf("%lld\n",ask(r)-ask(l-1));
    }
    

    2013 比赛

    题目描述

    点此看题

    解法

    遇到这种乱搞题我是真做不来,虽然所有需要的性质我都观察出来了

    首先我们需要有一个整体观念,因为要取模我们认为答案会超过 \(\tt int\),所以说就算你不进入不合法情况然后一个一个搜也会 \(\tt TLE\),那么我们要使用记忆化的技巧,明确这一点之后有如下剪枝:

    • 当一只球队的分数已经超过给定的分数时跳出。
    • 当一只球队如果赢下剩下的所有比赛分还是不够时也跳出。
    • 可以预先解方程计算出胜场数 \(nx\) 和平局数 \(ny\),搜索时必须符合此限制。
    • 由于每个人是等价的,我们在搜完一层(指决策完某个人的所有比赛)之后可以得到剩下人还需要的分数,那么我们把分数数组排序之后哈希,如果哈希值相同的情况方案数相同,所以这里可以记忆化。

    我觉得这种题唯一需要的就是勇气,很多看上去不强的剪枝可能有奇效。

    #include <cstdio>
    #include <iostream>
    #include <algorithm>
    #include <map>
    using namespace std;
    const int MOD = 1e9+7;
    #define int long long 
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,sum,nx,ny,a[15],c[15];map<int,int> mp;
    int dfs(int x,int y)
    {
    	if(x==n) return 1;
    	if(a[x]+3*(n-y+1)<c[x]) return 0;
    	if(y==n+1)
    	{
    		int b[15]={},s=0;
    		for(int i=x+1;i<=n;i++) b[i]=c[i]-a[i];
    		sort(b+1,b+1+n);
    		for(int i=x+1;i<=n;i++) s=30*s+b[i]+1;
    		if(mp.find(s)!=mp.end()) return mp[s];
    		return mp[s]=dfs(x+1,x+2);
    	}
    	int ans=0;
    	if(a[x]+3<=c[x] && nx)//win
    	{
    		a[x]+=3;nx--;
    		ans+=dfs(x,y+1);
    		a[x]-=3;nx++;
    	}
    	if(a[y]+3<=c[y] && nx)//lose
    	{
    		a[y]+=3;nx--;
    		ans+=dfs(x,y+1);
    		a[y]-=3;nx++;
    	}
    	if(a[x]+1<=c[x] && a[y]+1<=c[y] && ny)
    	{
    		a[x]++;a[y]++;ny--;
    		ans+=dfs(x,y+1);
    		a[x]--;a[y]--;ny++;
    	}
    	return ans%MOD;
    }
    signed main()
    {
    	n=read();
    	for(int i=1;i<=n;i++)
    		c[i]=read(),sum+=c[i];
    	sort(c+1,c+1+n);
    	nx=sum-n*(n-1);ny=n*(n-1)/2-nx;
    	printf("%lld\n",dfs(1,2)%MOD);
    }
    

    2013 数列

    题目描述

    点此看题

    解法

    这道题真的是要好好写写,自己的想法和正解根本没沾边。

    一开始我写出了一个根本就优化不来的容斥,但是我忽略了一个关键的条件:\(m(k-1)<n\),这说明不考虑首项的情况下,差分数组的总数是知道的,那么我们以他为切入点来分析问题。

    考虑一个差分数组 \(a\),那么它对答案的贡献是:

    \[n-\sum_{i=1}^{k-1}a(i) \]

    我们直接考虑计算总贡献:

    \[\sum_{w=1}^{m^{k-1}}n-\sum_{i=1}^{k-1} a_w(i)=n\cdot m^{k-1}-\sum_{w=1}^{m^{k-1}}\sum_{i=1}^{k-1} a_w(i) \]

    因为 \(a(i)\) 是互相独立并且在 \([1,m]\) 中自由选取的,所以后面一项的和是 \(m^{k-2}\cdot (k-1)\cdot\frac{m(m+1)}{2}\)

    #include <cstdio>
    #define int long long
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,k,p;
    int qkpow(int a,int b)
    {
    	int r=1;
    	while(b>0)
    	{
    		if(b&1) r=r*a%p;
    		a=a*a%p;
    		b>>=1;
    	}
    	return r;
    }
    signed main()
    {
    	n=read();k=read();m=read();p=read();
    	int ans=n%p*qkpow(m,k-1)%p-
    	qkpow(m,k-2)*(k-1)%p*((m+1)*m/2%p)%p;
    	printf("%lld\n",(ans+p)%p);
    }
    

    2013 旅行

    题目描述

    点此看题

    解法

    这道题太神了,我花了一晚上的时间才完全搞懂

    首先我们只考虑最小值是多少,先给结论吧,设 \(b_i\) 表示权值数组的后缀和,\(sum=b_1\)

    • \(sum=0\),设 \(tot\) 表示 \(b\) 数组中 \(0\) 点个数。如果 \(tot\geq m\),那么答案为 \(0\);如果 \(tot<m\),那么答案为 \(1\)
    • \(sum\not=0\),则答案 \(k=\lceil\frac{|sum|}{m}\rceil\)

    显然上述都是答案下界,下面给出这两个结论的构造性证明:

    对于第一个结论的第一部分是很显然的,我们考虑证明一下第二部分:

    考虑相邻的位置 \(1,-1\),如果我们把他们合并成 \(0\),得到新数列长度 \(-1\) 并且总和还是 \(0\),然后可以继续操作 \(0\) 或者操作 \(1,-1\) 直接数列长度为 \(m\),然后直接选取每个单个的数就构造出了答案。

    对于第二个结论,我们考虑分为 \(m\leq |sum|\)\(m>|sum|\) 两个部分来讨论。

    如果 \(m\leq |sum|\),那么原序列显然取遍 \([0,sum]\),我们可以从小到大取前缀和等于 \(k,2k,3k...sum\) 的这些断点分开,由于 \(k=\lceil\frac{|sum|}{m}\rceil\) 显然每一段权值 \(\leq k\) 并且段数正好为 \(m\)

    如果 \(m>|sum|\),我们可以类似地合并 \(-1,1\) 让整个序列只有 \(m\) 个数,并且按照我们的方法可以让这 \(m\) 的个数的绝对值都小于等于 \(1\),所以直接选取单个数即可。


    有了上面的结论我们可以按贪心的思路构造最小字典序的解,具体来说我们可以考虑找出当前所有合法的位置,然后选取其中 \(a\) 最小的,先考虑 \(sum\not=0\) 的做法,合法位置的充要条件可以考虑归纳法,就是只要后面还能满足数列的若干性质即可,设还剩 \(t\) 个端点,上一个端点是 \(i\),现在考虑的端点是 \(j\),那么条件是:

    \[\begin{cases} |b_{i+1}-b_{j+1}|\leq k\\ \frac{|b_{j+1}|}{t-1}\leq k\\ n-j\geq t-1 \end{cases} \]

    那么我们考虑分别解决这些条件并维护最小的 \(a\) 即可,观察到 \(b_{j+1}\) 出现了很多次,我们可以按权值分类,每一类权值都维护一个关于 \(a\) 递增的单调队列。每个增量一个端点的时候搜索 \([b_{i+1}-k,b_{i+1}+k]\) 这个区域之内的单调队列,并且同时判断第二个条件,选取最小的 \(a\) 即可,第三个条件在 \(t\) 减小的时候动态加入候选点即可。

    对于 \(sum=0\) 的情况也需要用单调队列只不过更为简单,在此就不详细展开。看上去我们暴力搜索复杂度很高,因为只会搜索 \(m\) 次所以复杂度其实是 \(O(mk)=O(n)\) 的。

    总结

    字典序问题不必枚举一个之后再暴力检验,在可以在保证后面合法的条件下选取最优点。

    在维护若干柿子的时候注意关注承接变量,可以让这个变量为纽带联系这些柿子。

    本题的结论和证明也很有意思,总之在权值连续变化的数列问题中,可以把它看成函数然后考虑函数的取值特点,这时候连续的条件往往会产生许多强烈的性质,我道听途说这个是介值定理

    #include <cstdio>
    const int M = 2000005;
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,d,tot,now,a[M],b[M],p[M],cnt[M];
    struct node
    {
    	int x,id;
    	node(int X=0,int I=0) : x(X) , id(I) {}
    	bool operator < (const node &b) const
    	{
    		return x<b.x;
    	}
    }t[M],ans;
    struct Q
    {
    	int l,r;
    	Q(int x=1){l=x;r=x-1;}
    	void push(node x)
    	{
    		while(l<=r && x<t[r]) r--;
    		t[++r]=x;
    	}
    	void get()
    	{
    		while(l<=r && t[l].id<now) l++;
    		if(l<=r && t[l]<ans) ans=t[l];
    	}
    }q[M];
    int Abs(int x)
    {
    	return x>0?x:-x;
    }
    void add(int x)
    {
    	q[b[x+1]+n].push(node(a[x],x));
    }
    signed main()
    {
    	n=read();m=read();
    	for(int i=1;i<=n;i++)
    		a[i]=read(),b[i]=(read()?1:-1);
    	for(int i=n;i>=1;i--)
    		b[i]+=b[i+1],cnt[b[i+1]+n]++,tot+=!b[i];
    	for(int i=0,s=1;i<=2*n;i++)
    		q[i]=Q(s),s+=cnt[i];
    	if(!b[1]) d=(tot<m);
    	else d=(Abs(b[1])-1)/m+1;
    	if(!d)
    	{
    		tot=0;now=1;
    		for(int i=1;i<=n;i++)
    			if(!b[i+1]) p[++tot]=i;
    		for(int i=1,j=1;i<m;i++)
    		{
    			while(j<=tot && tot-j>=m-i)//make sure enough 0-position
    				q[0].push(node(a[p[j]],p[j])),j++;
    			ans.x=M;q[0].get();now=ans.id+1;
    			printf("%d ",ans.x);
    		}
    	}
    	else
    	{
    		int r=now=1;
    		while(n-r>=m-1) add(r++);//vaild positions
    		while(m>1)
    		{
    			int sd=b[now]+n;ans.x=M;
    			for(int i=sd-d;i<=sd+d;i++)
    				if(Abs(i-n)<=d*(m-1)) q[i].get();
    			printf("%d ",ans.x);
    			now=ans.id+1;add(r++);m--;
    		}
    	}
    	printf("%d\n",a[n]);
    }
    
  • 相关阅读:
    PlayMaker 不支持过渡条件
    Unity Inspector 给组件自动关联引用(二)
    C# 重载和默认参数(那种情况下使用更好)
    Unity 开发游戏编写代码的技巧
    C# Conditional特性避免 预处理命令泛滥使用
    在DirectX12中使用Texture
    再探Lua的require
    如何实现水下效果
    n个骰子的点数
    用DirectX12实现Blinn Phong
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/15763995.html
Copyright © 2011-2022 走看看