本場鏈接:[Codeforces Round #666 (Div. 2)]
閑話
早上起來打的,CD整體不是那么難,就是稍微有點卡思維.
A. Juggling Letters
題目大意:給定\(n\)個不定長度的字符串,並且可以把里面的元素任意交換位置,問是否可以讓所有字符串都相等.注意先后長度可以變換.只需要輸出是否可行,不需要輸出具體方案.
數據范圍:\(1 \leq n \leq 1000\)
思路
顯然由於任意交換的條件過於強大,所以直接對所有的字符統計,並check字符的數量是否都是\(n\)的倍數,如果有一個不是就說明無法分配,即不能使所有相同.
代碼
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
ios::sync_with_stdio(0);cin.tie(0);
int T;cin >> T;
while(T--)
{
int n;cin >> n;
map<char,int> cnt;
for(int i = 1;i <= n;++i)
{
string s;cin >> s;
int len = s.size();
for(int j = 0;j < len;++j)
++cnt[s[j]];
}
int ok = 1;
for(char a = 'a';a <= 'z';++a)
if(cnt[a] % n)
{
ok = 0;
break;
}
if(ok) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
B. Power Sequence
題目大意:給定給一個長度為\(n\)的序列\(a\),以下標0為開始.定義一個序列是牛逼的,當且僅當序列\(a\)里的元素滿足\(a_i=c^i\).其中\(c\)為某個任意整數,只要有一個\(c\)滿足即可(即c不是指定的,存在一個c滿足即可).現在可以任意重排整個序列,並用\(1\)的代價使序列里任意一個元素的值增加一或減少一,問把整個序列變成一個牛逼的序列最少需要多少的代價.
數據范圍:
\(1 \leq n \leq 10^5\)
\(1 \leq a_i \leq 10^9\)
思路
第一步題目給定了一個重排的操作,顯然因為一個牛逼的序列一定是一個上升的序列(除了\(c=1\)的特殊情況,其他的都是嚴格上升的),所以一個比較直接的想法就是把整個序列按升序排序,再枚舉所有可能的\(c\),算出每個值的消耗,因為排序之后比不排序一定更好,所以算出來的結果一定正確.那么這里有一個問題:\(c\)的取值范圍是多少.
從直覺上來說,這個題目的數據范圍相當的大,\(c\)的取值也一定和\(a\)序列的和有關,而且可以猜到\(c\)的取值范圍是很有限的,而這個和可以到達\(10^{14}\).不過一個牛逼序列,同時是一個等比數列,在\(c\)增大的過程中整個牛逼序列的和會上升的非常快,因此上界會很快的達到.那么對於單個的\(c\)的上界,不妨就按\(1e18\)作為\(INF\),之后因為是一個等比數列,一共有\(n\)項,那么\(c\)的上界設置成是\(INF^{\frac{1}{n}}\),因為整個牛逼序列的和不能過大,這樣就可以保證了,當然這個上界並不准確,而且運算過程中可能會爆掉\(ll\).所以在具體寫的時候,是一步一步的計算出當前的和,如果已經超過了答案,那么就直接過掉,避免溢出.其次可以猜測這個復雜度是比較正常的,因為\(c\)並不會有多少個取值.
代碼
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+7;
const ll INF = 1e18;
int a[N];
int main()
{
int n;scanf("%d",&n);ll res = INF,s = 0;
for(int i = 1;i <= n;++i) scanf("%d",&a[i]),s += a[i];
sort(a + 1,a + 1 + n);
int limit = pow(INF,1.0/n);
for(ll c = 1;c <= limit;++c)
{
ll loc = llabs(1 - a[1]),k = 1;
for(int i = 2;i <= n;++i)
{
k *= c;loc += llabs(a[i] - k);
if(loc > res) break;
}
res = min(res,loc);
}
printf("%lld",res);
return 0;
}
C. Multiples of Length
題目大意:給定一個長度為\(n\)的序列.給定一種操作:每次選擇一個長度為\(len\)的區間,並且可以讓整個區間里的每一個數都減掉一個\(len\)的倍數(注意並不是所有的都減掉一個相同的倍數,而是可以不同的,並且可以減0倍).問是否可以在恰好三次操作之后把整個序列里的所有的數變成\(0\).注意是恰好三次,即使當前序列已經是\(0\)序列了,但是還是要恰好三次操作.
數據范圍:
\(1 \leq n \leq 10^5\)
\(-10^9 \leq a_i \leq 10^5\)
思路
由於操作數只有\(3\)次,因此如何快速的把所有的值全部消掉就成了這個題目的關鍵目標.首先一個簡單的想法就是看能不能直接把整個序列,即按\(n\)的倍數全部消掉.顯然這個序列一開始肯定不會是恰好都有\(n\)的倍數的,所以先預支一次操作嘗試把所有的元素變成\(n\)的倍數,那么變成多少呢.一個比較好的構造方式是把所有的元素變成\(n * a_i\)那么對每個數來說,需要增加一個\(a_i * (n - 1)\).想到這里,先把第一步操作具體出來:對\([1,n-1]\)的每個元素,增加自己的\(n-1\)倍,因為你想要增加\(n-1\)倍,那么長度只能是\(n-1\),不能直接對整個\(n\)長度的序列增加,這一點只能退而求其次.
那么現在還有兩次操作,\([1,n-1]\)的所有元素現在是\(n*a_i\),距離這個題目的終極目標:所有元素都要有\(n\)的公共因數,還差一個\(a_n\),那么由於可以讓增加的倍數是\(0\),就可以操作\([2,n]\)這個序列,並且不動前面的所有的元素,只動最后一個\(a_n\)把他如法炮制即可在兩次操作之后得到一個新序列,其中每個元素都是之前的元素的\(n\)倍.那么最后一次操作,直接把所有的元素減掉自己的\(a_i\)倍,就能讓所有的元素都是\(0\)了.恰好在三步完成.
不過分析到這里,可以想到一個邊界問題:當\(n=1\)的時候,顯然是沒有所謂的第一個元素和最后一個元素的,這種情況只能特判掉.需要特別注意一下.
代碼
我的代碼跟上面的思路實際上有一點出入,不過影響不大.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
const int N = 1e5+7;
int a[N];
signed main()
{
int n;scanf("%lld",&n);
if(n == 1)
{
scanf("%lld",&a[1]);
printf("1 1\n%lld \n",-a[1]);
printf("1 1\n0\n");
printf("1 1\n0\n");
return 0;
}
for(int i = 1;i <= n;++i) scanf("%lld",&a[i]);
printf("1 %lld\n",n-1);
for(int i = 1;i < n;++i) printf("%lld ",a[i] * (n - 1)),a[i] += a[i] * (n - 1);
printf("\n%lld %lld\n%lld",n,n,a[n] * (n - 1)),a[n] += (n - 1) * a[n];
printf("\n1 %lld\n",n);
for(int i = 1;i <= n;++i) printf("%lld ",-a[i]);
return 0;
}
D. Stoned Game
題目大意:有兩個人在玩博弈游戲,一共有\(n\)堆石子,每堆有\(a_i\)個石子.每次可以從任意一堆里選出一個石子,但是不能從對手上一輪選過的堆里選石子,首先不能操作的人輸.先手TL,后手H.輸出勝者.
數據范圍:
\(1 \leq n \leq 100\)
\(1 \leq a_i \leq 100\)
思路
拿到這個題,有一個很想當然的想法:既然不能選對手取過的堆,那么對先手的人來說,拿最多石子的堆顯然是最好的,因為選擇一個最多的石碓,顯然可以比對手更晚的選擇一個別的石碓,而選擇一個更少的會使局面不那么有利.那么對后手來說也是如此想當然的嗎.可以推理猜測一下,假設說后手並不選擇整個石子里面第二大的石碓,那么選擇一個較小的石碓會讓局面更不利,因為先手的人總是占着一個較大的石碓,他總是能占優勢,而快速的讓整個局面跳到只有兩堆石子或者只有一堆石子的時候都是不利的,因此后手也只能是選當前最大的石碓.
經過這樣的一個感性分析,可以確定兩個人的策略實際上都是選整個局面里面最大的堆,那么繼續往下可以發現整個局面的結果是唯一確定的,只需要維護一個\(pq\),並且記錄上一輪的人選擇的是哪一堆就可以模擬整個對局了.由於整個序列長度以及個數都非常的小,所以復雜度就可以亂搞搞.這個模擬做法也是跑得飛快.
代碼
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 105;
int a[N];
typedef pair<int,int> pii;
#define x first
#define y second
int main()
{
int T;scanf("%d",&T);
while(T--)
{
int n;scanf("%d",&n);
priority_queue<pii> pq;
for(int i = 1;i <= n;++i) scanf("%d",&a[i]),pq.push({a[i],i});
int cur = 1,last = -1;
while(!pq.empty())
{
pii _t = pq.top(),_t2;pq.pop();
int u = _t.x,p = _t.y;
if(p == last)
{
if(pq.empty()) break;
_t2 = pq.top();pq.pop();pq.push(_t);
u = _t2.x;p = _t2.y;
}
if(u == 0) break;
cur ^= 1;pq.push({--u,p});last = p;
}
if(cur) puts("HL");
else puts("T");
}
return 0;
}
E. Monster Invaders
題目大意:有個\(n\)列敵人,每一列有\(a_i\)個小兵和\(1\)個boss,血量分別為1和2.手上有三種武器,第一種對一個當前列的第一個敵人造成一點傷害,第二種對一列的敵人造成傷害,第三種直接消滅第一個敵人.三種武器的代價分別是\(r_1,r_2,r_3\).當你通過第一種或者第二種武器對boss造成傷害並且沒有直接消滅它的時候,你會被強制往相鄰的格子移動.移動的代價是\(d\).不允許在移動的同時射擊,每秒只能選擇一個武器射擊.求消滅所有敵人的最小花費.
數據范圍:
\(1 \leq n \leq 10^6\)
\(1 \leq d \leq 10^9\)
\(1 \leq r1 \leq r2 \leq r3 \leq 10^9\)
思路
首先注意到數據范圍比較大,還會爆int,先留個心眼.在模擬完樣例之后可以抽象這個題目的具體流程,發現他就是一個決策的過程,在某個位置,當前這一列的boss的血量和相應的決策會產生什么結果,自然可以嘗試\(dp\)求解.由於關鍵的信息就是當前在哪一列以及當前列的boss的血量,先就直接把這兩個信息放到數組里嘗試一下能不能做.
狀態:\(f[i][j]\)表示當前人在第\(i\)列,且當前列的boss血量為\(j\).不難想到說這個狀態划分是只和上一位的狀態有關的,也就是說最多有兩列相鄰的boss是存活的,因為再多的時候不如當時就解決掉.
入口:\(f[1][0] = a[1] * r_1,f[1][1] = min((a[1] + 1) * r_1,r_2)\)要注意的是第二種武器是同時會對boss造成傷害的.
那么這里就有一個問題了,這個狀態定義的是人就在第\(i\)列,那么從前一個位置轉移過來的時候,\(f[i - 1][1]\)應該要表示的是一個\(i-1\)列被攻擊了的狀態,但是如果\(i-1\)列被攻擊了,那么按照定義來說他是要被強制移動的,但是如果把這個\(d\)的花費加進來又有一個問題,因為你根本不知道這個位置是到了左邊還是到了右邊,因此一個解決辦法就是直接把他挪進轉移的表達式里再進行計算.
具體一點來說就是對於\(f[i][0]\)的計算里,如果要用到\(f[i-1][1]\)那么就額外的加一個\(d\)在他后面,同時在算\(f[i][1]\)的時候並不把\(d\)給他直接加上,因為那樣在嚴格的定義之下,他的位置是發生了變化的.我看別的題解並沒有對這一點進行說明,補充一點.
轉移:
-
先定義兩種最基本的操作:第一種是直接消滅整列的敵人,這個可以通過\(a[i]*r_1+r_3\)做到,其次是把boss打到一點血,這個時候實際上有兩種情況存在,要么就用第二種武器直接打到boss,要么就先用第一種武器\(a[i]\)次再加一次射擊boss:\(min((a[i]+1)*r_1,r_2)\).
-
對於\(f[i][0]\)來說
-
從前面一個全空的狀態轉移到一個全空的狀態,直接消滅所有敵人
\(f[i][0] = f[i - 1][0] + d + (a[i] + 1) * r_1 + r_3\)
-
從\(i-1\)剩余一個1血量的狀態轉移而來,首先要給\(f[i-1][1]\)加一個d,表示這個狀態被強制移動到了\(i\)位置,再對這個\(i\)列的boss打成殘血,那么這個時候就有兩種做法,也就是之前說的第二種基本操作,注意在這次操作之后會被強制移動,選擇移動到\(i-1\)再次增加一個\(d\),由於\(i-1\)的boss沒死,給他補一槍\(r_1\)再回到\(i\)又要增加一次\(d\)的移動代價,現在\(i\)列的boss也沒死需要再補一槍,於是再補一個\(r_1\)給這個地方.那么總的方程也就出來了.
\(f[i][0] = f[i - 1][1] + d + r_1 + d + min((a[i] + 1) * r_1,r_2) + d + r_1\)
-
同樣是從前面過來,只不過直接先消滅所有\(i\)列的敵人,那么前面的部分保持不變,還是\(f[i-1][1]+d\)之后來到了第\(i\)列,支付第一種基本操作的代價把\(i\)列清空,再走到\(i-1\)補一槍\(r_1\)最后再回到\(i\).
\(f[i][0] = f[i - 1][1] + d + r_1 + d + a[i] * r_1 + r_3 + d\)
-
-
對於\(f[i][1]\)來說
-
一定要摳定義,不能隨便增加條件,定義的就是人在\(i\)位置,那么就不能隨便變換.第一種是前面的一列就是空的,那么從前面一列走過來就沒什么負擔,不會出現多列都沒清空的情況,這部分走過來的代價就是\(f[i-1][0]+d\)那么還需要把第\(i\)列的boss打殘,需要支付第二個基本操作的代價.注意這里按題目意思來說是會被強制移動的,但是從定義出發你根本不知道要被移動到哪里,所以這里狀態計算的時候實際上沒有把這個強制移動的代價算進去,而是在前面說的,計算別的狀態的時候再把他補充進去考慮.
\(f[i][1] = f[i - 1][0] + min((a[i] + 1) * r_1,r_2) + d\)
-
那么另外一種就是先走過來,把第\(i\)列的射殘,走到\(i-1\)列補一槍,再走回\(i\)列.跟前面是類似的可以比對過來.
\(f[i][1] = f[i - 1][1] + d + r_1 + d + min((a[i] + 1) * r_1,r_2) + d\)
-
出口:
其中一個比較簡單就是\(f[n][0]\)另外一種應該是從\(f[n-1][1]\)轉移而來的,由於末尾一位移動的時候不能向后移動了,因此直接用\(f[n][0]\)不能覆蓋所有情況,還需要額外算一下另外一種\(f[n - 1][1] + d + r_1 + a[n] * r_1 + r_3\)
代碼
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6+7;
ll f[N][2],a[N];
int main()
{
ll n,r1,r2,r3,d;cin >> n >> r1 >> r2 >> r3 >> d;
for(int i = 1;i <= n;++i) cin >> a[i];
f[1][0] = a[1] * r1 + r3,f[1][1] = min((a[1] + 1) * r1,r2);
for(int i = 2;i <= n;++i)
{
ll op1 = a[i] * r1 + r3;
ll op2 = min((a[i] + 1) * r1,r2);
f[i][0] = f[i - 1][0] + op1 + d;
f[i][0] = min(f[i][0],f[i - 1][1] + 3 * d + 2 * r1 + op2);
f[i][0] = min(f[i][0],f[i - 1][1] + r1 + 3 * d + op1);
f[i][1] = f[i - 1][0] + op2 + d;
f[i][1] = min(f[i][1],f[i - 1][1] + op2 + 3 * d + r1);
}
ll res = min(f[n][0],f[n - 1][1] + (a[n] + 1) * r1 + r3 + 2 * d);
cout << res << endl;
return 0;
}
