2021.杭电中超 第二场.补题

2021 杭电多校第二场

  1. 1001题 https://acm.hdu.edu.cn/showproblem.php?pid=6961 签到

大意:T组测试样例,每组给定一个n 代表 边长为 n-1的立方体,求最多可以找到多少个不同的等边三角形,等边三角形的边必须平行于立方体的某个面。并且三角形的三个顶点坐标必须是整数。

思路:题目给了样例,当n=3时,即边长为2 的立方体,可以找到8个题目中所说的等边三角形。即通过连接立方体的顶点构成的等边三角形,当变成为2是,将大立方体可以划分成8个长度为1的小立方体 再加上变成为2的立方体构成的8个,共有 ( 2 3 2^3 23+1)* 8.

当 n=4时,划分成边长为 为 1的小立方体 有 3 3 3^3 33个,划分成变成为 2 的小立方体 有 2 3 2^3 23 个 所以共 ( 3 3 3^3 33+ 2 3 2^3 23+1) * 8;

所以公式即为 ( ( n − 1 ) 3 (n-1)^3 (n1)3+ ( n − 2 ) 3 (n-2)^3 (n2)3 +…+1 ) * 8 ,这题主要是有个公式 1 3 1^3 13+ 2 3 2^3 23+ 3 3 3^3 33+…+ n 3 n^3 n3= ( n ∗ ( n + 1 ) / 2 ) 2 (n*(n+1)/2)^2 (n(n+1)/2)2

总结:记住常用的数学公式。

  1. 1003题 https://acm.hdu.edu.cn/showproblem.php?pid=6963 最短路、博弈

大意:Alice 和 Bob 在一个无向图图上玩游戏,Alice 在 x 位置,Bob 在 y 位置,z代表终点位置。T组测试样例,n个点,编号1~n, m条边。Alice 获胜输出1 ,平局输出2,Bob胜输出3。游戏规则如下:Alice 先手,每次只能向走一步,可以选择走或不走。每当Bob走后算作一个回合。若在一个回合中,Alice 走到z,而Bob没有走到则Alice 胜,若Bob走到z,而Alice 走不到 则Bob 胜,若两者都能到或者都不到则平局。除了起点和终点,二人不能同时在同一个点。二人都采用尽量赢的策略,若赢不了则尽量平局。(n<=1e3)

思路:因为是最优策略。显然考虑最短路径。若二人都到不了z,显然是平局。如果x到z的最短路径小于y到z 的最短路径则显然 Alice 必胜,反之若大于,则Bob胜。接下来就是二者到z的最短距离相等的情况,对于这种情况,因为只会按照最短路径的方式去走,不是按照最短路去走显然不是最优策略。所以我们只需要把最短路径的图存下来就行了。然后用类似于SG的方式,利用记忆化搜索区去求结果。f[x,y,0/1]表示 Alice 在 x , Bob 在 y ,0代表轮到Alice 走,1代表轮到Bob走,并以最优策略操作的局面。 属性,1,2,3 分别代表Alice 胜,平局,Bob胜。记忆化搜索具体还有很多细节,具体看代码。求最短路的时候,因为边权都是1,所以直接用 bfs求就行了。另外还有个细节就是,在存最短路径图的时候,因为要采用最优策略,每次走都是朝着靠近x的方向走,否则则不是最优策略。所以存的是走向 x 的有向图,而不是无向图。记忆化搜索代码如下:

int dp(int x,int y,int z,int k) //记忆化搜索
{
    if (f[x][y][k]) return f[x][y][k];
    int tt;
    if(k==0)
    {
        if(x==z&&y==z)return 2;
        if(x==z&&y!=z)return 1;
        if(x!=z&&y==z)return 3;
        tt=3;
        for (auto i:v1[x])
          if ((i!=y||i==z))
          {
              int kk=dp(i,y,z,k^1);
              tt=min(tt,4-kk);   //  找到所有走一步的最优的一个
          }
        f[x][y][k]=tt;
    }
    else
    {
        tt=3;
        for (auto i:v1[y])
          if ((i!=x||i==z))
          {
              int kk=dp(x,i,z,k^1);
              tt=min(tt,4-kk);
          }
        f[x][y][k]=tt;
    }
    return f[x][y][k];
}

总结:树上的博弈很有意思,其实仔细分析之后感觉并不是特别难,另外用动态规划的角度去做博弈之前没做过。

  1. 1004 I love counting https://acm.hdu.edu.cn/showproblem.php?pid=6964 字典树+树状数组

大意:有一个含 n 个数的序列 c ( c i c_i ci<=n),现有 m 次查询,每次给定一个区间 [l,r]以及两个整数 a,b。问区间内有多少种 c ,注意不是多少个 c,满足 a ⨁ \bigoplus c<=b。(n,m<=100000)

思路:如果没有左端点的限制,我们还是比较好求的,把查询的右端点从小到大排个序,然后边插入字典树,边查询,对于重复出现的c,我们不插入就好了。也就是说对于左端点都是从1开始的我们是很好计算种类数的。但是对于此题的它查询的是一段区间就很麻烦了,我们该怎么办呢? 经典操作:将区间问题转化成前驱问题,即类似于前缀和的方法,我们令 s[x] 表示从 1 到 x 的种类数,区间[l,r]的种类数也就是 s[r]-s[l-1]. 这样我们就可以通过在每个下标的位置都建立一颗字典树,然后利用前缀和的方式算出一段区间的种类数了。现在考虑怎么建立字典树,如果我们每次都是在前面的字典树的基础上再插入当前位置的c, 这样虽然能否做到直接查询,但是空间复杂度直接就炸掉了,如果我们只以当前位置的c建立一颗字典树的话,虽然空间复杂度不会炸,但是如果我们不加优化的话求和的时间复杂度就是O(n)了,也是不行的。有什么办法能快速求和呢?树状数组!!用一个树状数组就好了!!即在树状数组中建立字典树,这样求和就变成了logn,再算上字典树中的logn , m次查询,时间复杂度就是O( n ( l o g n ) 2 n(logn)^2 n(logn)2​​)。对于重复元素我们只需要把之前出现过的删掉,然后在当前右边界插入,这样就可以保证每次查询不会重复计算相同的c了。 但是因为我们已经把前面的给删去了,查询的区间如果还是在前面那段的话就会出问题,所以先把查询的区间存下来,根据右端点边更新边离线查询。具体看代码:

#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int c[N];
int rt[N]; //纪录每个 c[i] 建立的字典树的根节点的位置
int son[N*400][2],idx; //n颗字典树,每课字典树最多 17 * logn 个节点
int sum[N*400];//纪录每个节点c的个数
struct node{
	int l,r,a,b,id;
};      // 记下查询操作
int pre[N]; // 纪录某个 c 是否在前面出现过
int ans[N]; // 纪录答案
vector<node> q[N];
void insert(int id,int x) // 在 下标为 id 的位置建立字典树
{
	if(!rt[id])rt[id]=++idx;
	int p=rt[id];
	for(int i=16;i>=0;i--)
	{
		int u=x>>i&1;
		if(!son[p][u])son[p][u]=++idx;
		p=son[p][u];
		sum[p]++;
	}
}
void del(int id,int x)
{
	int p=rt[id];
	for(int i=16;i>=0;i--)
	{
		int u=x>>i&1;
		p=son[p][u];
		sum[p]--;
	}
}
int query(int id,int a,int b) // 查询下标为 id 的位置的字典树满足 a^c<=b 的个数
{
	int p=rt[id];
	int res=0;
	for(int i=16;i>=0;i--)
	{
		int x=b>>i&1;
		int y=a>>i&1;
		if(x) // b 这一位是 1 的话
		{
			res+=sum[son[p][y]]; //和 y 相同的分支一定是可以的
			p=son[p][y^1]; // 和 y 不同的分支可能可以
		}
		else p=son[p][y];  // b 这一位是 0 的话,只能往和 y 相同的分支走
		if(!p)break;
	}
	if(p)res+=sum[p];
	return res;
}
int lowbit(int x)
{
	return x&-x;
}
void add(int id,int x)  // 树状数组,在 id 位置添加 x
{
	while(id<=n)
	{
		insert(id,x);
		id+=lowbit(id);
	}
}
void remove(int id,int x) //删除之前出现过的
{
	while(id<=n)
	{
		del(id,x);
		id+=lowbit(id);
	}
}
int ask(int id,int a,int b)  // 查询
{
	int res=0;
	while(id)
	{
		res+=query(id,a,b);
		id-=lowbit(id);
	}
	return res;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	insert(0,0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>c[i];
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		int l,r,a,b;
		cin>>l>>r>>a>>b;
		q[r].push_back({l,r,a,b,i}); // 以右端点为查询
	}
	for(int i=1;i<=n;i++)
	{
		if(pre[c[i]])remove(pre[c[i]],c[i]);
		add(i,c[i]);
		pre[c[i]]=i;
		for(auto &p:q[i])ans[p.id]=ask(i,p.a,p.b)-ask(p.l-1,p.a,p.b);
	}
	for(int i=1;i<=m;i++)cout<<ans[i]<<"\n";

	return 0;
}
  1. 1005 I love string https://acm.hdu.edu.cn/showproblem.php?pid=6965 签到

大意:给定一个长度为 n 的字符串,从字符串中从前往后每次取一个字符插到新串的开头和结尾,新串最初是空串,空串没有开头和结尾,问在字典序最小的情况下,有多少种构成新串的方式。

思路:签到题,对于此题我们会发现,一个字符串只有所有字母都一样时,插入字符时才没有字典序大小之分,否则,在开头和结尾插入造成的字典序大小必然是不同的,所以我们只需要统计开头有多少个连续相同的字符就行了,答案就是2的多少次方。

  1. 1008 I love exam https://acm.hdu.edu.cn/showproblem.php?pid=6968 dp 01背包,分组背包

大意:有 t 天复习时间,有n门功课,最多可以挂 p 门课,现在每门科目给出一些复习方案,需要花费时间以及提高一定的分数,每门科目的分数认为从 0 开始,低于60分认为被记为挂科,求满足条件的情况下能获得的最大总分。

思路:单看每门科目,显然是个01背包问题,我们跑完01背包之后,就将问题转化成了,有 n 组,每组中有若干个物品,从每组中最多选一个物品,显然是个分组背包问题面,但是还有挂科数的限制,所以我们多加一维纪录挂科数就好了。所以对于此题其实也就是先跑一遍01 背包,然后在跑一遍分组背包就好了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef  pair<int,int> PII;
#define v first  
#define w second
const int inf=0x3f3f3f3f;
int n,m,t,p;
int f[55][510],dp[2][510][5];
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	int _;
	cin>>_;
	while(_--)
	{
		map<string,int> mp;
		cin>>n;
		for(int i=1;i<=n;i++)
		{
			string s;
			cin>>s;
			mp[s]=i;
		}
		cin>>m;
		vector<PII> g[55];
		for(int i=1;i<=m;i++)
		{
			string s;
			int w,v;
			cin>>s>>w>>v;
			g[mp[s]].push_back({v,w});
		}
		cin>>t>>p;
		memset(f,0,sizeof f);
		for(int i=1;i<=n;i++)
			for(auto &x:g[i])
				for(int j=t;j>=x.v;j--)
					f[i][j]=min(max(f[i][j],f[i][j-x.v]+x.w),100);
		

		memset(dp,-0x3f,sizeof dp);

		for (int j = 0; j <= t; ++j) 
		{
	        if (f[1][j] >= 60) dp[0][j][0] = f[1][j];
	        else dp[0][j][1] = f[1][j];
    	}

		int now=0;
		for(int i=2;i<=n;i++)
		{
			for(int j=t;j>=0;j--)//分组背包滚动数组从大到小枚举体积
				for(int k=0;k<=j;k++) // 枚举分组
					for(int gk=0;gk<=p;gk++) //挂科数
					{
						if(f[i][k]<0)continue;
						if(dp[now][j-k][gk]<0)continue;
						dp[now^1][j][gk+(f[i][k]<60)]=max(dp[now^1][j][gk+(f[i][k]<60)],dp[now][j-k][gk]+f[i][k]);
					}
			for(int j=t;j>=0;j--)   // 因为有挂科数的影响,所以需要人为把上一层的清空
					for(int gk=0;gk<=p;gk++)
						dp[now][j][gk]=-inf;
			now^=1;
		}

		int ans = -1;
	    for (int i = 1; i <= t; ++i) {
	        for (int j = 0; j <= p; ++j) {
	            ans = max(dp[now][i][j], ans);
	        }
	    }
    	cout << ans << endl;

	}
	return 0;
}

总结:对于背包问题关键在于对问题的转化拆分,找到题目描述的属于那种问题,进而采取对应的dp分析方式。

  1. 1011 I love max and multiply https://acm.hdu.edu.cn/showproblem.php?pid=6971 思维题,dp预处理,分组

大意:给定 A 序列,B序列 定义C 序列 C k = m a x ( A i ∗ B j ) C_k=max(A_i*B_j) Ck=max(AiBj)​​ 下标 i,j,k 满足 i&j>=k。 求C序列各个数的和的最大值。

思路:因为是与运算i&j≥k,对于某个k的,它的二进制表示某些位是1,对于i,j只要对应的位也是1,其他位是0还是1无所谓,他们 与运算 的结果一定是不小于k的,也就是说我们只关注下标某些二进制位是不是1就行了。所以我们预处理的时候把下标的二进制形式有相同的二进制位是1的放到同一个集合,维护最大值最小值。通过上面的方式 我们一定集合去更新一定能包含 i&j==k的情况,但可能会漏掉 一些 i&j>k的情况,所以可以从后往前更新,i&j>k,也就等价于 i&k>=(k+1)。

代码如下:

#include <bitsdc++.h>
using namespace std;
typedef long long LL;
const LL N=1<<21,M=998244353;
LL f1[N],f2[N],g1[N],g2[N],a[N],b[N],ans[N];
int n;
int main()
{
    int _;
    scanf("%d",&_);
    while(_--)
    {
        scanf("%d",&n);
        for(int i=0;i<n;i++)scanf("%lld",&a[i]),f1[i]=f2[i]=a[i];
        for(int i=0;i<n;i++)scanf("%lld",&b[i]),g1[i]=g2[i]=b[i];
        for(int i=n-1;i>=0;i--)
        {
            for(int j=0;j<19;j++)
            {
                if(!((i>>j)&1))
                {
                    int p=i|(1<<j);
                    if(p<n)
                    {
                        f1[i]=max(f1[i],f1[p]);
                        f2[i]=min(f2[i],f2[p]);
                        g1[i]=max(g1[i],g1[p]);
                        g2[i]=min(g2[i],g2[p]);
                    }
                }
            }
        }
        ans[n]=-1e18;
        LL res=0;
        for(int i=n-1;i>=0;i--)
        {
            ans[i]=ans[i+1];
            ans[i]=max(ans[i],f1[i]*g1[i]);
            ans[i]=max(ans[i],f1[i]*g2[i]);
            ans[i]=max(ans[i],f2[i]*g1[i]);
            ans[i]=max(ans[i],f2[i]*g2[i]);
            LL p=0;
            res+=ans[i];
            res%=M;
        }
        res=(res+M)%M;
        printf("%lld\n",res);
    }
    return 0;
}

总结:对于和二进制有关的题目,多从二进制的角度去想想,从二进制的角度出发。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值