2024 ICPC National Invitational Collegiate Programming Contest, Wuhan Site解题记录
I,K,B,E都比较水,就不写了
F:Custom-Made Clothes:
交互题,直接求解第k大有难度,由于对于任意一个点(x,y),只能确定左下与右上部分的数与它的大小关系,对于左上与右下部分的数,无法确定其大小关系,这时扭转思路,想到可以试验数val是否为第k大,由此想到二分答案,并通过单调关系验证其为第几大,从而得出答案。
D:ICPC
dp题,难点在于状态转移过程。
性质1:最优的走法一定可以是只走一个方向或只转一次方向
考虑设计dp状态,不难设计:$$dp[i][j]为起始于第i个点,走j步$$ 考虑转移过程:
- 首先考虑只走一个方向,可以用前缀和快速计算出只走一个方向(往左或往右)的最大值
- 考虑只转一个方向:对于第i个点,由于其有先向右走再往左走或先往左走再往右走两种可能,故其需要i-1点,i+1点的状态来转移,这显然是无法设计转移顺序的。因此,我们考虑先考虑先左后右的情况,再考虑先右再左的情况,将两种情况分开,确定方向,则可以设计转移顺序。
- 对于先左后右的情况:\(dp[i][j]基于dp[i - 1][j - 1],因为无论最优情况是向左多少步,其一定会经过i -1,且最优情况一定会被i - 1的最优情况包含(因为先左后右的情况下必定会回到并重新经过i,其实相当于多走了一步重复的)\)
- 对于先右后左的情况:同理
初始状态对,转移方法对,且转移顺序可设计且正确,则dp正确。
code:
#include <bits/stdc++.h>
using namespace std;
const int N = 5010;
long long a[N],dp[N][N << 1];
long long pre[N];
long long sg[N][N << 1];
int n;
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> n;
for(int i = 1;i <= n;i++)
{
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}
int l,r;
for(int i = 1;i <= n;i++)
{
for(int j = 0;j <= (n << 1);j++)
{
l = max(i - j ,1);
r = min(i + j,n);
sg[i][j] = max(pre[i] - pre[l - 1],pre[r] - pre[i - 1]);
}
}
for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= (n << 1);j++)
{
dp[i][j] = max(sg[i][j],dp[i - 1][j - 1]);
}
}
for(int i = n;i >= 1;i--)
{
for(int j = 1;j <= (n << 1);j++)
{
sg[i][j] = max(sg[i][j],sg[i + 1][j - 1]);
dp[i][j] = max(sg[i][j],max(dp[i][j],dp[i + 1][j - 1]));
}
}
long long ans = 0;
for(int i = 1;i <= n;i++)
{
long long temp = 0;
for(int j = 1;j <= (n << 1);j++)
temp ^= (j * dp[i][j]);
temp += i;
ans ^= temp;
}
cout << ans << endl;
return 0;
}
C:TreeBag and LIS
构造题就是找到一种合理的自限方式(或多种),使结果可控。
十分神奇的构造题,首先这种要凑 和恰好为某一特定数的题目,优先想到exgcd(走完凑和的最后一公里路)。
接下来考虑如何用\(10^5\)个数字来凑\(10^{13}\) 的范围。首先,既然题目要找LIS,那一种非常合理的思路就是先自限LIS的长度,易得LIS的长度为1和2时都无法达到\(10^{13}\) 的范围,则考虑LIS的长度为3的情况。
我们希望将LIS分别限制再一个区间内,这样可以保证LIS间互不干扰,不会出现前面LIS中的数跟后面的LIS中的数产生新LIS,那么一种办法就是将大的数放前面,小的放后面,这样前面大的数就在后面区间外找不到更大的数来构成LIS.即:$$ 7...78....89...9|5...56...67....7|3....34....45...5|0.....0230....012 $$
明显发现最后一段0123数字构成的序列与前面有不一样的规律,其原因是前面都是为了快速将和变大,使其能逼近\(10^{13}\)的范围,但最后一段就要确保和恰好为需要的数了,这时候再用前面的方法无法控制,那么我们考虑利用前置0来用0的个数来控制23与12的个数,但因前面的0也可与后面的12构成LIS,故实际上是由35与12来凑最后的和来确保和恰好为特定数,此处使用exgcd即可。需要注意的是,当剩下的和大于等于35 * 12时才能保证一定有非负整数解(可行解),故要提前预留35*12的余量。若x < 35 * 12,直接纯1构造即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
long long n,gg;
const int f[10] = {0,789ll,567ll,345ll,23ll,12ll};
int tim[10][10];
int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
void exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1ll;
y = 0ll;
} else {
exgcd(b, a % b, y, x);
y -= a / b * x;
}
}
pair<int, int> solve(int a, int b, int c) {
int x, y;
exgcd(a, b, x, y);
x *= c; y *= c;
int d = gcd(a, b); // 最大公约数
int k = 0ll;
while (true) {
int a_val = x + b * k;
int b_val = y - a * k;
if (a_val > 0 && b_val > 0) {
int tt = y / (a * k);
b_val = y - (a * k) * tt;
a_val = x + (b * k) * tt;
return make_pair(a_val, b_val);
}
k++;
}
}
int check(pair <int,int> pp)
{
int tsum = 0;
for(int k = 1;k <= 2;k++)
{
int te = 1;
for(int i = 1;i <= 3;i++)
te *= tim[k][i];
tsum += f[k] * te;
}
tsum += pp.first * 35 + pp.second * 12;
if(tsum == gg)
{
cout << "Accepted" << endl;
return 1;
}
else
{
cout << "fuck" << endl;
cout << tsum << " " << gg << " " << gg - tsum << endl;
return 0;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
//freopen("in.txt","w",stdout);
cin >> n;
gg = n;
if(n > 35ll * 12ll + 1)
{
n -= (35ll * 12ll + 1);
for(int i = 1;i <= 3;i++)
{
int temp = f[i];
if(temp > n)continue;
tim[i][1] = tim[i][2] = tim[i][3] = 1;
int ntim = 1;
int wtim = 0;
for(int cnto = 0;;cnto = (cnto + 1) % 3)
{
int cnt = cnto + 1;
//cout << "cnt:" << cnt << endl;
if(wtim > 3)break;
ntim = ntim * (tim[i][cnt] + 1)/ tim[i][cnt] ;
if(ntim * temp <= n)
{
tim[i][cnt]++;
wtim = 0;
continue;
}
else
{
ntim = ntim * tim[i][cnt]/ (tim[i][cnt] + 1) ;
wtim++;
continue;
}
}
n -= ntim * temp;
}
n += 35 * 12 + 1;
pair <int,int> pa = solve(35,12,n);
//if(n == pa.first * 35 + pa.second * 12)cout <<"exgcd accepted" << endl;
//else cout << "exgcd wrong:" << n << endl;
//int flag = check(pa);
//if(!flag)
//{
//return 0;
//}
for(int i = 1;i <= tim[1][1];i++)
cout << 7;
for(int i = 1;i <= tim[1][2];i++)
cout << 8;
for(int i = 1;i <= tim[1][3];i++)
cout << 9;
for(int i = 1;i <= tim[2][1];i++)
cout << 5;
for(int i = 1;i <= tim[2][2];i++)
cout << 6;
for(int i = 1;i <= tim[2][3];i++)
cout << 7;
for(int i = 1;i <= tim[3][1];i++)
cout << 3;
for(int i = 1;i <= tim[3][2];i++)
cout << 4;
for(int i = 1;i <= tim[3][3];i++)
cout << 5;
//cout << pa.first * 35 + pa.second * 12 << endl;
if(pa.first != 0)
{
for(int i = 1;i <= pa.first;i++)
cout << 0;
cout << 23;
}
if(pa.second != 0)
{
for(int i = 1;i <= pa.second;i++)
cout << 0;
cout << 12;
}
}
else
{
if(n == 0)
{
cout << 0;
}
else
{
for(long long i = 1;i <= n;i++)
{
cout << 1;
}
}
}
return 0;
}
G:Pack
除法分块,难点在于将题意转化为数学式子,用一个简洁恰当的数学式子表达题意,根据题意,首先,需要满足装的方案和恰好为k,如此不难想到使用exgcd构造一组解(这场比赛咋这么爱exgcd),设这组解为\(k=a*x+b*y\) ,那么,显然,所有可能解都可以表示为\((x+k*Δx,y+k*Δy)\) ,那么对于每一组 \((x_i,y_i)\),不难得到其最多装\(min(n/x_i,m/y_i)\) 组,则最后剩下\(n+m-(x_i+y_i)*min(n/x_i,m/y_i)\)件物品。那么对于枚举所有的\((x_i,y_i)\)显然不可行,注意到n,m的范围也仅有\(10^9\) ,又有\(n/x_i,m/y_i\)存在,不难想到除法分块将需要枚举的个数限制在\(\sqrt{n} + \sqrt{m}\) 次内,对于同一个分块内,即对于每一段\(n/x_i,m/y_i\)不变的\((x_i,y_i)\),直接计算\(x_i+y_i\)的最大值即可,因为组数不变,每组装得越多剩下得越少。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,m,val1,val2;
int k;
int T;
int d1,d2,n1,n2;
int exgcd(int a, int b, int& x, int& y) {
if (b == 0)
{
x = 1, y = 0;
return a;
}
int g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
pair <int,int> solve(int A,int B,int C) {
int x0, y0;
int g = exgcd(A, B, x0, y0);//a,b的最大公约数
if (C % g != 0)
{
cout << "-1\n";
}
int a = A / g, b = B / g, c = C / g;
x0 *= c;y0 *= c;//这一步求出来的x0,y0才是ax+by=c的一个解
/*int rx=x0%b,ry=y0%a;
if(rx<=0)
rx+=b;
if(ry<=0)
ry+=a;*/
b = abs(b);
a = abs(a);
d1 = b;
d2 = a;
if(x0 < 0)
{
int tt = (0 - x0) / b;
x0 += tt * b;
y0 -= tt * a;
}
if(y0 < 0)
{
int tt = (0 - y0) / a;
x0 -= tt * b;
y0 += tt * a;
}
while(1)
{
if(x0 >= 0 && y0 >= 0)break;
if(x0 < 0)
{
x0 += b;
y0 -= a;
}
else
{
x0 -= b;
y0 += a;
}
}
int tt = x0 / b;
x0 -= tt * b;
y0 += tt * a;
return make_pair(x0,y0);
}
int now1,now2,nxt1,nxt2;
int flag = 1;
void init()
{
int temp;
now1 = n1;
if(now1 == 0)
{
nxt1 = 1;
}
else
{
temp = n / now1;
nxt1 = n / temp + 1;
}
now2 = n2;
temp = m / now2;
nxt2 = m / (temp + 1);
}
void sol()
{
if(now1 > n || now2 < 1)
{
flag = 0;
return;
}
//cout << "begin" << endl;
int temp;
int g1 = nxt1 - now1;
int g2 = nxt2 - now2;
g2 = abs(g2);
int b1 = g1 % d1 == 0 ? g1 / d1 : g1 / d1 + 1;
int b2 = g2 % d2 == 0 ? g2 / d2 : g2 / d2 + 1;
//cout << b1 << " " << d1 << " " << b2 << " " << d2 << endl;
if(b1 <= b2)
{
now1 += b1 * d1;
now2 -= b1 * d2;
if(now1 > n || now2 < 1)
{
flag = 0;
return;
}
if(now1 == 0)
{
nxt1 = 1;
}
else
{
temp = n / now1;
nxt1 = n / temp + 1;
}
if(now2 == 0)
{
flag = 0;
return;
}
temp = m / now2;
nxt2 = m / (temp + 1);
}
else
{
now1 += b2 * d1;
now2 -= b2 * d2;
if(now1 > n || now2 < 1)
{
flag = 0;
return;
}
if(now1 == 0)
{
nxt1 = 0;
}
else
{
temp = n / now1;
nxt1 = n / temp + 1;
}
if(now2 == 0)
{
flag = 0;
return;
}
temp = m / now2;
nxt2 = m / (temp + 1);
}//cout << "end" << endl;
if(now1 > n || now2 < 1)
{
flag = 0;
return;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> T;
while(T--)
{
now1 = now2 = nxt1 = nxt2 = 0;
flag = 1;
d1 = d2 = n1 = n2 = 0;
cin >> n >> m >> val1 >> val2;
cin >> k;
if(val1 < val2)
{
swap(val1,val2);
swap(n,m);
}
//cout << val1 << " " << val2 << endl;
pair <int ,int> pp = solve(val1,val2,k);
n1 = pp.first;//开始极小
n2 = pp.second; /// 开始极大
init();//cout << now1 << " " << now2 << endl;
int ans = 0;
while((now1 == 0 ||(now1 * now2 > 0 && n / now1 >= m / now2 ))&& flag)
{
ans = max(ans,(now1 + now2) * (m / now2));
//cout << ans << " " << now1 << " " << now2 << endl ;
sol();
}
if(now2 != 0)
{
while(flag && (n / now1 < m / now2 && now1 * now2 > 0) )
{
//cout << "session2 :"<< now1 << " " << now2 << endl;
ans = max(ans,(now1 + now2) * (n / now1));
//cout << ans << " " << now1 << " " << now2 << endl ;
sol();
}
}
if(k % val1 == 0)
{
int tt = k / val1;
int g = n % tt;
ans = max(ans,n - g);
}
cout << n + m - ans << endl;
}
return 0;
}
M:Merge
注意到合并是x + (x + 1),不难得到每一次合并出来的和都是奇数,不可能合出偶数,那也就意味着每一次合并都需要一个原序列中的偶数,不难想到将原序列的偶数从大到小排序,依次判断是否能凑出使该偶数发生合并的奇数,再对原序列数是否使用进行一些维护即可。