“蔚来杯“2022牛客暑期多校训练营2

C.Link with Nim Game

题意:

nim游戏的稍微加强版。只不过此时必胜的人想尽快胜,必输的人想尽量慢的输。
问游戏可进行多少轮及先手在第一轮有多少种操纵。

思路:

首先应该知道nim游戏。

整体异或和为0时,先手必输。异或和非0时,先手必胜。

考虑先手必输,由于输的人想尽量慢的输,贪心考虑是否可以拿掉1个,且留给后手的最优策略同样是拿

掉一个。可以发现这种情况一定可以存在。取整体二进制的最后一位1即可,后手只能镜像操纵。那么轮

数就可以确定了,为 ∑ i = 1 n a [ i ] \sum_{i = 1} ^ {n} a[i] i=1na[i]。考虑先手有多少种操纵才能使得先手拿掉1个,后手的最优策略也是拿掉

一个。可以发现只要满足当前lowbit位为1,且当前组内所有此二进制位为1的和这个数相同即可,才能保证

后手最优策略一定是拿掉1个。
X3 2 1 0
A1 1 0 0
B1 1 1 0
C1 1 0 0
D1 1 1 0

假设先手从A拿掉1个,那么后手因为想快点赢,那么完全可以从B拿,使得B变为(1000),此时是B的最优

策略。

考虑先手必胜,由于胜的人想尽量胜,那么最优策略肯定是在满足留给后手为异或为0的局面的情况下,

拿的石头还要尽量多。此时轮到后手,后手也可采取拿掉1个,导致先手也只能拿掉1个才能使得异或值

非0,轮数为 ∑ i = 1 n a [ i ] − m a x ( a [ i ] − a [ i ]    x o r    x ) + 1 \sum_{i = 1} ^ {n} a[i] - max(a[i] - a[i] \;xor \;x) + 1 i=1na[i]max(a[i]a[i]xorx)+1(x为整体异或和),那么第一轮的先手操纵为 $max

(a[i] - a[i] ;xor ;x)$的次数

typedef pair <int, int> PII;
ll a[maxn];
ll lowbit(ll x)
{
	return x & -x;
}
int main()
{
	int T;
	scanf("%d", &T);
	while(T --)
	{
		int n;
		scanf("%d", &n);
		ll sum = 0, res = 0;
		for(int i = 1 ; i <= n ; i ++)
		scanf("%lld", &a[i]), res ^= a[i], sum += a[i];
		if(res == 0)
		{
			int cc = 0;
			for(int i = 0 ; i < 31 ; i ++)
			{
				int cur = -1;
				bool flog = true;
				int cnt = 0;
				for(int j = 1 ; j <= n ; j ++)
				{
					ll t = lowbit(a[j]);
					if(__lg(lowbit(t)) == i)
					cur = t;
				}
				if(cur > 0)
				{
					for(int j = 1 ; j <= n ; j ++)
					{
						if(a[j] >> i & 1)
						{
							if(lowbit(a[j]) == cur)
							cnt ++;
							else 
							{
								flog = false;
								break;
							}
						}
					}
					if(flog)
					cc += cnt;
				}
			}
			printf("%lld %d\n", sum, cc);
		}
		else // res ! = 0
		{
			ll maxx = 0;
			int ccnt = 0;
			for(int i = 1 ; i <= n ; i ++)
			{
				if(a[i] - (a[i] ^ res) == maxx)
				ccnt ++;
				else if(a[i] - (a[i] ^ res) > maxx)
				{
					maxx = a[i] - (a[i] ^ res);
					ccnt = 1;
				}
			}
			printf("%lld %d\n", sum - maxx + 1, ccnt);
		}
	}
	return 0;
}

D.Link with Game Glitch

题意:

给定n个点和m条有向边,每k * a[i]个b[i]类物品可以换k * c[i]个d[i]类物品。求最大的w(0 <= w < 1)使得不存在一种交换方式得到无限多的物品。
0 < = n < = 1 e 3 , 2 < = m < = 2 e 3 0 <= n <= 1e3, 2 <= m <= 2e3 0<=n<=1e3,2<=m<=2e3
1 < = b [ i ] , d [ i ] < = n , b [ i ]    ! =    d [ i ] 1 <= b[i], d[i] <= n, b[i] \;!= \;d[i] 1<=b[i],d[i]<=n,b[i]!=d[i]
a [ i ] , c [ i ] < = 1 e 3 a[i],c[i] <= 1e3 a[i],c[i]<=1e3

思路:

赛时一眼二分。 假设w越大,那么得到无限多的物品就越多。

考虑如何check。

赛时思路,利用有向图tarjan缩点,找环,对环中每个点为起点,开始跑dfs,再次回到起点时,比起点

的初始值大,那么就会得到无限多的物品。由于两个物品点之间存在多条边,要对每条边得到的值取

max。只有取到max,才能尽量得到无限多的物品,使得最终二分到的w尽量大,其次考虑到a[i],c[i] <=

1e3,会爆掉longlong,赛时不知道怎么处理。
l o g a + l o g b = l o g a ∗ b , l o g a − l o g b = l o g a / b loga + logb = log a * b, loga - logb = log a / b loga+logb=logablogalogb=loga/b

可以取log,把乘法改为加法,防止爆掉。

赛时思路过于繁琐。实际上在取log的基础上,跑SPFA,判断是否能松弛n次即可(类似于判负环)。

只不过这里要判正环。以每个点为起点,初始化点的权值为1,取log,变为0。

code:

int h[maxn], ne[maxn], e[maxn], idx, cnt[maxn];
double w[maxn], dis[maxn];
bool book[maxn];int n, m;
void add(int u, int v, double ww)
{
    w[idx] = ww;
    e[idx] = v;
    ne[idx] = h[u];
    h[u] = idx ++;
}
bool check(double mid)
{
    mid = log(mid);
    queue <int> alls;
    for(int i = 1 ; i <= n ; i ++)
    {
        alls.push(i);
        book[i] = true;
        dis[i] = 0;
        cnt[i] = 0;
    }
    while(!alls.empty())
    {
        int t = alls.front();
        alls.pop();
        book[t] = false;
        for(int i = h[t] ; i != -1 ; i = ne[i])
        {
            int j = e[i];
            if(dis[j] < dis[t] + w[i] + mid)
            {
                dis[j] = dis[t] + w[i] + mid;
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= n) return false; //产生了INF 
                if(!book[j])
                {
                    book[j] = true;
                    alls.push(j);
                }
            }
        }
    }
    return true;
}
int main()
{
     
    scanf("%d %d", &n, &m);
    memset(h, -1, sizeof(h)), idx = 0;
    for(int i = 1 ; i <= m ; i ++)
    {
        double a, c;    int b, d;
        scanf("%lf %d %lf %d", &a, &b, &c, &d);
        add(b, d, log(c / a));
    }   
    double l = 0, r = 1;
    while(r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if(check(mid)) l = mid;
        else r = mid;
    }
    printf("%.8lf\n", l);
    return 0;
}

H Take the Elevator

题意:

在一个k层楼,有一部电梯,现在共n个人,想从a[i]到b[i]层,电梯一次最多带m个人。且电梯在下降时,只有到达第一层,才会再次向上。且送完n个人后电梯要回到第一层的最小时间。
1 < = n , m < = 2 e 5 , 1 < = k < = 1 e 9 1 <= n, m <= 2e5, 1 <= k <= 1e9 1<=n,m<=2e5,1<=k<=1e9
a [ i ] ! = b [ i ] 且 a [ i ] , b [ i ] < = k a[i] != b[i] 且 a[i], b[i] <= k a[i]!=b[i]a[i],b[i]<=k

思路:

考虑到数据范围,那么正解时间复杂度为O(n) O(nlogn)。

O(n)貌似实现不了。考虑O(nlogn)

看到最小时间,第一眼二分。

但是发现貌似不能二分。

考虑贪心。

考虑向上的电梯,假设此时存在一个最高楼层为x,那么从1–>x必须安排一趟,且因为要求总时间最小,那么肯定是在用这一次的

代价(x - 1)基础上,尽量上楼层高的m个人。可以先按照y从大到小,且x从到小排序,先选取最高的m个人,此后可以发现电梯最后

一段上一定是这m个人,但是前一段,可以另外再次上电梯,只不过不能和最后一段的m个人发生冲突。要保证此时的y <= m个

人的x的最大值即可,x取max,可使得前一段上电梯的人尽量多,假设当前前一段有人上了电梯,那么其前前段还会存在上电梯

的,但是不能和他们冲突。

用multiset维护上/下楼的区间、优先队列维护此时最后一段m个人的y值即可。不断考虑是否前一段时间是否存在可以上电梯(不

发生的冲突下能上就上)。下楼类似。总的时间复杂度为O(nlogn)

code:

typedef pair <int, int> PII;
struct note{
	int x;
	int y;
	bool operator <(const note &a) const{
		if(y != a.y) return y > a.y;
		else return x > a.x;
	}
};int n, m, k;
multiset <note> up, down;
void cal()
{
	priority_queue <int> pq;
	int t = up.size();
	for(int i = 1 ; i <= min(m, t) ; i ++)
	{	
		pq.push((*up.begin()).x);
		up.erase(up.begin());
	} //先把m个人拿出来 
	while(pq.size()) 
	{
		int temp = pq.top();
		auto it = up.lower_bound({temp, temp});
		if(it == up.end())
		break; //最大的x都满足不了 
		else
		{
			pq.pop();
			pq.push((*it).x); //替换掉 
			up.erase(it);
		}
	}
	while(pq.size()) pq.pop();
	
	t = down.size();
	for(int i = 1 ; i <= min(m ,t) ; i ++)
	{
		pq.push((*down.begin()).x);
		down.erase(down.begin());
	}
	while(pq.size())
	{
		int temp = pq.top();
		auto it = down.lower_bound({temp, temp});
		if(it == down.end())
		break;
		else
		{
			pq.pop();
			pq.push((*it).x);
			down.erase(it);
		}
	}
}
int main()
{
	
	scanf("%d %d %d", &n, &m, &k);
	for(int i = 1 ; i <= n ; i ++)
	{
		int a, b;
		scanf("%d %d", &a, &b);
		if(a < b)
		up.insert({a, b}); 
		else // a > b
		down.insert({b, a});
	}
	ll ans = 0;
	while(up.size() || down.size())
	{
		ll t = 0;
		if(up.size()) t = max(t, (*up.begin()).y * 1ll);
		if(down.size()) t = max(t, (*down.begin()).y * 1ll);
		cal();	
		ans += (t - 1ll) * 2ll;
	}
	printf("%lld\n", ans);
	return 0;
}

K Link with Bracket Sequence I

题意:

给定一个括号序列a长度为n,且a是合法括号序列b长度为m的子序列。
求有多少种合法的括号序列b。对结果mod(1e9 + 7)
1 < = n , m < = 200 1 <= n, m <= 200 1<=n,m<=200
∑ n < = 1 e 3 \sum n <= 1e3 n<=1e3

思路:

由于之前没有写过由子序列—>原序列的奇数DP。

赛时一直总以为是排列组合。但是实在是无法去重。

也想过按照数据范围,进行DP。但是也不知道如何去重。

由子序列S构造原序列T问方案数这类问题。

如果在T中确定了T[i],匹配的是S中第p个字符S[p]

且在T中确定了T[j],匹配的是S中第p + 1个字符S[p + 1]。

除了保证T[i] =S[p], T[j] = S[p + 1]外,还要保证,T[i + 1] ~ T[j - 1]所有字符外,都和T[i]不同。才能保证方案不重复

本质上就是一种排列组合。可按照这种思想进行DP求解附加条件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EyfGiQlX-1658923583717)(C:\Users\Syl\AppData\Roaming\Typora\typora-user-images\image-20220724184454697.png)]
f[i][j][k]:新串中前i个和原串中前j个是否匹配过且当前前缀和为k的集合。

code:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;
typedef long long ll;
const int maxn = 1e3 + 10;
char str[maxn];
int f[3][210][210];
const int mod = 1e9 + 7;
int get_mod1(int a, int b)
{
	return (a % mod + b % mod) % mod;
}
int main()
{
	int T;
	scanf("%d", &T);
	while(T --)
	{
		int n, m;
		scanf("%d %d", &n, &m);
		scanf("%s", str + 1);
		memset(f, 0, sizeof(f));
		f[0][0][0] = 1;
		for(int i = 1 ; i <= m ; i ++)
		{
			for(int j = 0 ; j <= n ; j ++)
			{
				for(int k = 0 ; k <= m / 2; k ++)
				{
					if(j > 0)
					{
						f[i & 1][j][k] = 0;
 						if(str[j] == ')' && k + 1 <= m / 2)
						f[i & 1][j][k] = f[i - 1 & 1][j - 1][k + 1];
						else if(str[j] == '(' && k - 1 >= 0)
						f[i & 1][j][k] = f[i - 1 & 1][j - 1][k - 1];
						if(str[j] == '(' && k + 1 <= m / 2)
						f[i & 1][j][k] = get_mod1(f[i & 1][j][k], f[i - 1 & 1][j][k + 1]);
						else if(str[j] == ')' && k - 1 >= 0)
						f[i & 1][j][k] = get_mod1(f[i & 1][j][k], f[i - 1 & 1][j][k - 1]);
					} 
					else 
					{ //  f[1][0][1] = 
						f[i & 1][j][k] = 0;
						if(k - 1 >= 0)
						f[i & 1][j][k] = f[i - 1 & 1][j][k - 1];
						if(k + 1 <= m / 2)
						f[i & 1][j][k] = get_mod1(f[i & 1][j][k], f[i - 1 & 1][j][k + 1]);
					}	
				}
			}
		}
		printf("%d\n", f[m & 1][n][0]);
	}
	return 0;
}

J Link with Arithmetic Progression

题意:

给定一个数组n,可随机修改a[i]的值,但是每个i,最多修改一次,要求把整个数组变成等差数列。修改后的总代价为 ∑ i = 1 i = n ( a [ i ] − a [ i ] ′ ) 2 \sum_{i = 1} ^ {i = n}(a[i] - a[i]') ^ {2} i=1i=n(a[i]a[i])2。可把a[i]修改为小数。求满足条件的最小代价。
∑ n < = 1 e 6 \sum n <= 1e6 n<=1e6
− 1 e 9 < = a [ i ] < = 1 e 9 -1e9 <= a[i] <= 1e9 1e9<=a[i]<=1e9

思路:

解法来自"蔚来杯"2022牛客暑期坐牢训练营2

最开始看到这道题,毫无头绪。

无论是首项还是公差d,都有可能变化。 之后发现是求最小代价和不需要具体方案数

可向推式子上想想。题目给出了代价的式子。

a[i]’ = (i - 1) d + a[1] 等差数列,其实等差数列就是一条直线方程。

代入式子 ∑ i = 1 i = n ( a [ i ] − a [ i ] ′ ) 2 \sum_{i = 1} ^ {i = n}(a[i] - a[i]') ^ {2} i=1i=n(a[i]a[i])2

发现无论是假设首项a[1]为定值还是公差d为定值,式子都为一个凹函数(二次函数)。存在唯一的极值点。

那么假设三分首项a1。唯一的变量为d。

对于每一个i:

( a [ i ] − a [ i ] ′ ) 2 (a[i] - a[i]') ^ {2} (a[i]a[i])2

= ( i − 1 ) 2 ∗ d 2 + 2 ( i − 1 ) ( a [ 1 ] − a [ i ] ) d + a [ i ] 2 + 2 a [ i ] a [ 1 ] + a [ 1 ] 2 =(i - 1) ^ {2} * d ^ {2} + 2(i - 1)(a[1] - a[i])d + a[i] ^ {2} + 2a[i]a[1] + a[1] ^ {2} =(i1)2d2+2(i1)(a[1]a[i])d+a[i]2+2a[i]a[1]+a[1]2

式子中仅有d为变量。为标准的二次函数。且开头向上。

假设首项a[1]固定,对于每一个i都有一个二次函数,且每个函数极值点就是对称轴,但是每个函数极值点都不一样。

考虑如何选取一个公共的极值点。这里的做法是把n条二次函数合成为1条二次函数。 n个二次函数的二

次项系数相加,一次项系数相加。最后再对这条二次函数取对称轴。(具体证明不太会,但是题目那么多数据都能过,也能侧面

证明可行吧)

code:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <set>
#include <vector>
#include <cmath>

using namespace std;
//#define int long long
#define NO {puts("NO") ; return ;}
#define YES {puts("YES") ; return ;}
typedef pair<int, int> PII;
const int N = 2e5 + 10 , INF = 0x3f3f3f3f;
int n, m;
int a[N];

namespace GTI {
    char gc(void) {
        const int S = 1 << 16;
        static char buf[S], *s = buf, *t = buf;
        if (s == t) t = buf + fread(s = buf, 1, S, stdin);
        if (s == t) return EOF;
        return *s++;
    }
    int gti(void) {
        int a = 0, b = 1, c = gc();
        for (; !isdigit(c); c = gc()) b ^= (c == '-');
        for (; isdigit(c); c = gc()) a = a * 10 + c - '0';
        return b ? a : -a;
    }
}
 
using GTI::gti;

double calc(double a1) 
{
    double p = 0, q = 0;
    for (int i = 1; i <= n; i ++) 
    {  // 代入公式
        p += 1.0 * (i - 1) * (i - 1);
        q += 2.0 * (a1 - a[i]) * (i - 1); 
    }
    double d = -q / (2 * p); // 下凸二次函数的极值点(对称轴)
    double res = 0;
    for (int i = 1; i <= n; i++) 
        res += (a[i] - a1 - 1.0 * (i - 1) * d) * (a[i] - a1 - 1.0 * (i - 1) * d);
    return res;
}


void solve()
{
    n = gti();
    for(int i = 1 ; i <= n ; i ++ ) a[i] = gti();
    double l = -2e10 , r = 2e10;
    while(r - l > 1e-5)
    {
        double len = r - l;
        double mid_l = l + len / 3, mid_r = r - len / 3;
        if(calc(mid_l) >= calc(mid_r)) l = mid_l;
        else r = mid_r;
    }
    printf("%.20lf\n", calc(r));
}

signed main()
{
    ios::sync_with_stdio(0),cin.tie(0);
    int T = 1;
    T = gti();
    while(T -- ) solve();
    return 0;
}

L Link with Level Editor I

题意:

给定n张图,编号为1–n,每张图上有m个点,每个图上各有l[i]条边。每次操纵,要么在当前点要么原地不动直接进入下一张图,要么从当前点走向相邻的的节点,然后进入下一张图。要求选出最少的连续的子串使得能从1号点到达m号点。
n < = 1 e 4 , 2 < = m < = 2 e 3 n <= 1e4, 2 <= m <= 2e3 n<=1e4,2<=m<=2e3
∑ l < = 1 e 6 \sum l <= 1e6 l<=1e6

思路:

最开始一直以为是图上跑DP,但是无法建拓扑序。。

实际上可以发现,每次都是以1号点为起点,要么在原地不动,要么从1号点走向相邻节点,更新相邻节

点,然后进入下一张图,只有从1号点更新到的点才是有效点,每个去更新有效点的dp值,已经被算出来

了。按照题目数据进行dp,已经满足了拓扑序。

f[i][j]:在第i张图上到达j号点的起点所属图的集合。

属性:max

时间复杂度看似是 n * m + n * l,铁过不去。

但是实际上发现 ∑ l < = 1 e 6 \sum l <= 1e6 l<=1e6,所以时间复杂度为O(n * m + l)

初始化f[i][1] = i

原地不动: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] ) 原地不动: f[i][j] = max(f[i - 1][j]) 原地不动:f[i][j]=max(f[i1][j])
走到相邻的点 : f [ i ] [ j ] = m a x ( f [ i − 1 ] [ k ] ) ( k − − − > j ) 走到相邻的点: f[i][j] = max(f[i - 1][k]) (k---> j) 走到相邻的点:f[i][j]=max(f[i1][k])(k>j)

code:

const int maxn = 1e5 + 10;
typedef pair <int, int> PII;
int f[3][3010];
int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    for(int i = 0 ; i <= 1 ; i ++)
    {
        for(int j = 0 ; j <= m ; j ++)
        f[i][j] = -INF;
    }
    f[0][1] = 0;
    int res = INF;
    for(int i = 1 ; i <= n ; i ++)
    {
        int l;
        scanf("%d", &l);
        for(int j = 1 ; j <= m ; j ++)
        {
            if(j == 1) f[i & 1][j] = i;
            else f[i & 1][j] = -INF;
            f[i & 1][j] = max(f[i & 1][j], f[i - 1 & 1][j]);
        }
        for(int j = 1 ; j <= l ; j ++)
        {
            int u, v;
            scanf("%d %d", &u, &v);
            f[i & 1][v] = max(f[i & 1][v], f[i - 1 & 1][u]);
        }
        if(f[i & 1][m] != -INF)
        {
            res = min(res, i - f[i & 1][m]);
        }
    }
    if(res == INF) printf("-1\n");
    else printf("%d\n",res);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值