buctoj2021年ACM竞赛班训练(七)题解

A: 玩游戏

题意:初始有一堆石子共n个,双方轮流行动,每次可以从中取出恰好完全平方数(1、4、9……)个石子,不可以不取石子直接跳过回合。双方都足够聪明,会按最优的方式来游玩,无法行动的人输掉该游戏(等价说法:取走最后一个石子的人赢)。
思路
首先考虑一下必胜态,当n = sqrt(n) * sqrt(n) 的时候一定是必胜态,
那么再考虑一下必败态,是不是说如果f[i]这个状态是一个必胜态的话,那么f[i - j * j]一定是必败态,那么我们怎么知道f[i - j * j]这个状态到底是必胜态还是必败态呢,是不是可以用动态规划,用答案去更新答案。
设 f[i]表示在 i 堆石子时,当前操作的玩家是否有必胜策略。f[i]=true
为必胜,否则必败。
初始时,对于每一个i * i <= N , 都有 f[i*i]= true
转移时,对于每一个 i,枚举 j 满足 j * j ≤ i。如果存在 j 使得 f[i−j * j] 是必败的,则 f[i]=true。否则,f[i]=false。
状态转移方程:if(f[i - j * j ] == 0) f[i] = 1;
最终答案为 f[n]。
时间复杂度: O nsqrt(n)

#include
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define re register int
typedef long long ll ;
using namespace std;
const int N =  1e6 + 10 , M = 1010 , inf = 0x3f3f3f3f , mod = 1e9 + 7 ;
int t ;
bool f[N] ;
int main()
{
     
    cin >> t ;
    int n ;
    for(int i = 1 ; i * i <= N ; i ++) f[i*i] = true ;
    for(int i = 1 ; i <= N ; i ++)
    	for(int j = 1 ; j * j <= i ; j ++)
    		{
     
    		    if(f[i - j * j] == 0) f[i] = 1 ;
    		}
	 
    		
    while(t--)
    {
     
    	scanf("%d",&n);
    	printf("%d\n",f[n]);
	}
    return 0;
}

B:取石子游戏 2

题意:有n堆数量不定的石子,游戏双方轮流取石子,每人每次选一堆石子,并从中取走若干颗石子(至少取1颗),如果轮到某人取时已没有石子可取,那此人算负。
思路没啥可说的,我把推导过程补充一下吧。

先手必胜状态:可以一步走到先手必败状态
先手必败状态:无论怎样也走不到先手必败状态(留给对手的必然是必胜状态)

a1^ a2……^ an ==0 则先手必败
a1^ a2……^ an ==x!=0 先手必胜

证明:先a1^ a2……^ an == x 一定可以一步走到 a1^ a2……^ an == 0:
假设x二进制第一位为 1 是第 k 位,那么必定存在ai的第 k 位为 1
因为ai>ai^ x 那么先手拿走ai的 (ai-(ai^ x)个之后就变为了 ai-(ai-(ai^ x))=ai^x
则a1^ a2…ai^ x^ a(i+1)…^ an==x^x ==0

证明: a1^ a2……^ an == 0一定不能一步走到 a1^ a2……^an == 0:(反证)
假设改变数量的是ai,那么a1^ ……^ ai^ ……an==a1^ ……^ ai’^……an ==0 ;

然后左右两个等式左右异或(自己用异或性质想下) 则 ai==ai’与条件矛盾

因为是最优解,所以 a1^ a2……^an== x!=0 的人一定可以将式子为操作为全零给对方
而 a1^ a2……^ an== 0 无论怎么操作都只能返回 a1^ a2……^ an== x!=0
而数字是一定不断减少的,所以 a1^ a2……^an== 0 一定会变为a1== a2==……== an== 0
时间复杂度:O(n)

#include 
#include 
#include 
using namespace std;

int main() {
     
	int n;
	scanf("%d", &n);
	int res = 0;
	while (n--) {
     
		int x;
		scanf("%d", &x);
		res ^= x;
	}
	if (res)
		puts("win");
	else
		puts("lose");
	return 0;
}

C: 子串查找

题意:这是一道模板题。
给定一个字符串 A 和一个字符串 B,求 B 在 A 中的出现次数。A 和 B 中的字符均为英语大写字母或小写字母。
A 中不同位置出现的 B 可重叠。
思路:滚动哈希解题,H(C’)=H(C,k+n) - H(C,k) × bn,要预求得b,就能在O(1)时间内得到任意字符串的字符串的子串哈希值,从而完成字符串匹配。
hash详解
时间复杂度:o n

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
typedef long long ll ;
typedef unsigned long long ull ;
#include 
#include 
#include 
#include 
using namespace std;
ll N=1e6+5,mod=1e9+9;
ull f[1000005],l1,l2,t,K=103;
ull ji[1000005];
ull get(ll x,ll y)
{
     
    return f[y]-f[x-1]*ji[y-x+1];
}
int main()
{
     
    char a[N],b[N];
    ll i,j,k,m,ans=0,lena,lenb,t=0;
    ji[0]=1;
    for(i=1;i<=N;i++)
    {
     
        ji[i]=ji[i-1]*103;
    }
    scanf("%s%s",b+1,a+1);
    lena=strlen(a+1);
    lenb=strlen(b+1);
    for(j=1;j<=lenb;j++)
    {
     
        f[j]=f[j-1]*K+(b[j]-'A');
    }//计算主串的滚动哈希值
    for(j=1;j<=lena;j++)
    {
     
        t=t*K+(a[j]-'A');
    }//计算匹配串的哈希值
    for(j=1;j+lena-1<=lenb;j++)
    {
     
        if(get(j,j+lena-1)==t)
        {
     
            ans++;
        }//枚举起点为i,长度为n的子串,判断与匹配串是否匹配
    }
    printf("%lld\n",ans);
    return 0;
}

D: Power Strings

题意:给给定若干个长度≤106的字符串,询问每个字符串最多是由多少个相同的子字符串重复连接而成的。如:ababab则最多有3个ab连接而成。
思路:经典KMP模式匹配,用kmp模板对字符串本身进行匹配标记出p数组,且注意存在性质,S[1到i]具有长度为len kmp详解
时间复杂度:o n

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
typedef long long ll ;
#include 
#include 
#include 
#include 
using namespace std;
const int N=1e6+5,mod=10000;
char a[N];
ll p[N],b[N],que[N],len,ans,cnt;
void kmp()
{
     
    ll i,j=0;
    p[1]=0;
    for(i=1;i<len;i++)
    {
     
        while(j>0&&a[j+1]!=a[i+1])
        {
     
            j=p[j];
        }
        if(a[j+1]==a[i+1])
        {
     
            j++;
        }
        p[i+1]=j;
    }
}
int main()
{
     
    while(scanf("%s",a+1)!=EOF)
    {
     
        if(a[1]=='.')
        {
     
            goto loop;
        }
        len=strlen(a+1);
        kmp();
        ans=len/(len-p[len]);
        if(len%(len-p[len])==0)
        {
     
            printf("%lld\n",ans);
        }
        else
        {
     
            printf("1\n");
        }
    }
    loop:return 0;
}

E:找位置

题意:对一个数组进行修改或者询问a[i]<=c的下标最大值。
思路:数据结构东子哥也讲了,就是线段树,就是维护区间最小值,需要注意的地方就是在查询时需要先对又半边查询后对左半边进行查询,因为需要求得的是符合条件的元素下标的最大值。
线段树详解
时间复杂度:O(nlogn)

#include 
#include 
#include 
using namespace std;
const int N = 1000010;
int n, m;
int s[N];

struct Node {
     
	int l, r;
	int v;
} tr[N << 2];
// 由子节点的信息,来计算父节点的信息
void pushup(int u) {
     
	tr[u].v = min(tr[u << 1].v, tr[u << 1 | 1].v);
	return ;
}
//建树
void build(int u, int l, int r) {
     
	tr[u] = {
     l, r};

	if (l == r) {
     
		tr[u].v = s[l];
		return ;
	}

	int mid = l + r >> 1;
	build(u << 1, l, mid);
	build(u << 1 | 1, mid + 1, r);

	pushup(u);
	return ;
}
//查询
int query(int u, int l, int r, int c) {
     
	if (l == r)
		return l;
	int mid = tr[u].l + tr[u].r >> 1;
	if (tr[u << 1 | 1].v <= c)
		return query(u << 1 | 1, mid + 1, r, c);
	else
		return query(u << 1, l, mid, c);

}
//修改
void modify(int u, int x, int c) {
     
	if (tr[u].l == tr[u].r)
		tr[u].v = c;
	else {
     
		int mid = tr[u].l + tr[u].r >> 1;

		if (x <= mid)
			modify(u << 1, x, c);
		else
			modify(u << 1 | 1, x, c);

		pushup(u);
	}
}

int main() {
     
	scanf("%d %d", &n, &m);

	for (int i = 1; i <= n; i++)
		scanf("%d", &s[i]);

	build(1, 1, n);

	int t, x, c;
	while (m--) {
     
		scanf("%d", &t);
		if (t == 1) {
     
			scanf("%d %d", &x, &c);
			modify(1, x, c);

		} else {
     
			scanf("%d", &c);
			if (tr[1].v > c)
				printf("-1\n");
			else
				printf("%d\n", query(1, 1, n, c));
		}
	}
	return 0;
}

F: M爷的线段树

题意:一个长度为n的数列A。
修改m次,每次给区间[L,R]中的每一个数加X。
查询k次,每次查询第i个元素的值并输出。
思路:区间修改,单点查询,可以完美用前缀和+差分。
已知前缀和 S[n], 构造 b[n]
满足条件: S[i] = b1 + b2 + … + b[n]
差分就是前缀和的逆运算
b[1] = a[1]
b[2] = a[2] - a[1]
b[3 ]= a[3] - a[2]

b[n] = a[n] - a[n-1]
b[n]称为 S[n]的差分
S[n]称为 b[n]的前缀和
差分的用途
对 S数组的某个区间内的数全部加上 c,
差分帮助我们处理一种操作, 在 S数组的 [l, r]区间内加上数 c. 则

  1. a[l] ~ a[L-1]无影响
  2. a[l] ~ a[r] 加上了 c
  3. a[r+1] ~ a[n] 无影响

S数组操作后 对于 b数组的影响, 相当于 b[l] += c, b[r + 1] -= c
之后在对差分数组求一遍前缀和,就是修改之后的a数组,单点查询直接输出a[i]即可
时间复杂度:O n

#include
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define re register int
typedef long long ll ;
using namespace std;
const int N =  1e5 + 10 , M = 1010 , inf = 0x3f3f3f3f , mod = 1e9 + 7 ;
int a[N] , b[N] ;
int n , m , k ;
int main()
{
     
    while(cin >> n >> m >> k , n != -1 || m != -1 || k != -1)
    {
     
        fer(i,1,n) scanf("%d",&a[i]) ;
        
        fer(i,1,n) b[i] = a[i] - a[i-1] ;
        
        while(m--)
        {
     
            int x , y , c ;
            scanf("%d %d %d",&x,&y,&c) ;
            b[x] += c;
            b[y + 1] -= c;
        }
        
        fer(i,1,n)
            a[i] = b[i] + a[i-1] ;
            
        while(k--)
        {
     
            int x ;
            scanf("%d",&x) ;
            printf("%d\n",a[x]) ;
        }
    }
    return 0;
}

你可能感兴趣的:(buctoj,算法)