专门记录一点思维题。
1. 反转与双端队列:LC 2810 故障键盘
这题是个easy,但用StringBuilder强行模拟反转就没意思了。
可以这么想,当打出一个i,代表一个控制信号,反转。
一开始我们向队列尾部(屏幕上的输出)拼接字符。但一旦反转了,我们就在队列首部拼接字符,再反转就再在尾部拼接。
import java.util.ArrayDeque;
import java.util.ArrayList;
class Solution {
public String finalString(String s) {
char[] ch = s.toCharArray();
ArrayList<Character> dq = new ArrayList<Character>();
boolean tail = true;
for (char c : ch) {
if(c=='i'){
tail = !tail;
continue;
}
if(tail){
dq.add(c);
}else{
dq.add(0,c);
}
}
StringBuilder sb = new StringBuilder();
if(tail){
for (int i = 0; i < dq.size(); i++) {
sb.append(dq.get(i));
}
}else{
for (int i = dq.size()-1; i >= 0; i--) {
sb.append(dq.get(i));
}
}
return sb.toString();
}
}
2. 剥洋葱:LC 2811 判断是否能拆分数组
1和2都来自周赛357,质量挺高,但可惜我摆了没打(
这题观察到一个性质就秒杀:如果有一个长度恰好为2的子数组的元素和≥m,那么就可以成功。
例如:
[ 1,1,1,1,2,2,1,1 ] m = 4
我们可以发现有个[2,2]的子数组,既然它已经满足了元素和≥4,那么它带上它左边或右边的一段子数组,依然可以满足,因为每个元素都是≥0的。比如说我们可以把这个东西拆成:
[1]
[ 1,1,1,2,2,1,1]
然后这样从前从后一直拆,类似于剥洋葱,拆到[2,2]也没问题,再把[2,2]拆了就行。
所以就是说,当数组的长度>2的时候,如果有两个相邻的数之和≥m。就能成。
import java.util.List;
class Solution {
public boolean canSplitArray(List<Integer> nums, int m) {
if(nums.size()<=2){
return true;
}
for (int i = 0; i < nums.size()-1; i++) {
if(nums.get(i)+nums.get(i+1)>=m){
return true;
}
}
return false;
}
}
3. 先全部拿出来:LC 2171 拿出最少数目的魔法豆
这道题我使用前后缀做的,跑的速度其实一般。这里看了其他人的做法后学会了一种新的思考方式。
首先排序是必须的(后面会知道为什么
我们可以直接先把所有的魔法豆拿出来,然后再枚举分界线。
对于任意索引i,由于排过序,所以<beans[i]的,也就是[0,i-1]的,全部清零。但是对于[i,n-1],不应清空,则要归还(n-1-i+1) = (n-i)个beans[i]。这样也是O(n),但是常数时间好很多。
class Solution {
public long minimumRemoval(int[] beans) {
Arrays.sort(beans);
int n=beans.length;
long sum=0L,ans= Long.MAX_VALUE;
for(int bean : beans) sum+=bean;
for(int i=0;i<n;++i){
ans=Math.min(ans, sum-(long)(n-i)*beans[i]);
}
return ans;
}
}
4. 分类讨论:LC 3012 通过操作使数组长度最小
双周赛122 T3。坐牢1h没思路。
在删除的时候,不要求大小关系,那么如果让小的数模大的,就相当于白删一个大的数保留小的。比如:
[ 1,3 ]
那么就可以1%3=1,删除1,3加入1,这就相当于白删一个3。
如果最小的数只有这样的一个,就相当于可以直接把所有其它的数全删了,达到理论上的绝对最短数组,1个元素。
如果最小的数有多个,怎么办呢?核心想法就是看能否造出来一个新的最小的值:
- 举个例子 2,2,3,4,答案可不是[1,0],而是[1],可以3,4造一个1出来,然后用1把剩下两个2都删了。或者2,3造一个1出来也可以。那么怎么造呢?假设最小值是m,如果存在x,x%m≠0,那么很显然x%m<m,就造出来一个新的最小值了。
- 但如果所有其余的x%m=0怎么办?也就是所有其余的x都是m的倍数。那么是不可能造出来<m的数的,于是整个数组就剩下了所有的m,这个时候只能两两组队,往数组里增加0了。
有这个思路,代码写起来非常短:
import java.util.Arrays;
class Solution {
public int minimumArrayLength(int[] nums) {
int min = Arrays.stream(nums).min().getAsInt();
int cnt = 0;
for (int num : nums) {
if(num%min!=0){
return 1;
}else if(num==min){
cnt++;
}
}
return (int) Math.ceil((double)cnt/2);
}
}
如果能造出来一个更小的数,那么直接返回1就可以。否则就是所有的min组队造0,上取整是min为奇数个的情况。
5. 扩散:LC 2808 使循环数组所有元素相等的最少秒数
这个数组最终要每个元素相等,那么最终相等的那个元素一定取自于原数组(因为是把一个元素变为自身或左右相邻的元素),所以我们枚举每一种生成元。维护该生成元的所有索引。
假设生成元记为x,其他元素均记为y。假设数组长成:
[ x,y,y,x,x,y,y,x,y,y,y ]
那么要用x扩散到整个数组,就是所有相邻x的距离的最大值除以2上取整(因为一秒钟内可以左边扩散一下,右边扩散一下)。在以上例子中,最长距离是3,第一个x和最后一个x(别忘了是环形的)。所以最长扩散时间就是上取整3/2=2。
维护最短的扩散时间即可。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
int n;
public int minimumSeconds(List<Integer> nums) {
n = nums.size();
HashMap<Integer, List<Integer>> m = new HashMap<>();
for (int i = 0; i < nums.size(); i++) {
Integer num = nums.get(i);
List<Integer> l;
if(!m.containsKey(num)){
l = new ArrayList<>();
}else{
l = m.get(num);
}
l.add(i);
m.put(num,l);
}
int min = Integer.MAX_VALUE;
for (Map.Entry<Integer, List<Integer>> e : m.entrySet()) {
int time = calTime(e.getValue());
min = Math.min((time +1)/2,min);
}
return min;
}
private int calTime(List<Integer> l){
if(l.size()==1){
return (n-1);
}
int max = Integer.MIN_VALUE;
for(int i=1;i<l.size();i++){
max = Math.max(max,l.get(i)-l.get(i-1)-1);
}
max = Math.max(l.get(0) +n-1-l.get(l.size()-1),max);
return max;
}
}
6. 最大频次:LC 3039 进行操作使字符串为空
最后一次操作前剩下的字符串就是那些最高频词的字母遗留下来的最后一个字符。
所以做两件事情:
- 统计每个字母的出现频次
- 统计每个字母的最后出现位次(这是为了保证最终形成的字符串各字母相对位置正确)
然后排个序就可以了。总体O(nlgn)。
import java.util.Arrays;
class Solution {
public String lastNonEmptyString(String s) {
int maxFreq = 0;
int[][] m = new int[26][3];
for (int i = 0; i < 26; i++) {
m[i][2] = i;
}
char[] ch = s.toCharArray();
for (int i = 0; i < ch.length; i++) {
m[ch[i]-'a'][0]++;
maxFreq = Math.max(maxFreq,m[ch[i]-'a'][0]);
m[ch[i]-'a'][1] = i;
}
Arrays.sort(m, (o1, o2) -> {
if(o1[0]==o2[0]){
return Integer.compare(o1[1],o2[1]);
}
return -Integer.compare(o1[0],o2[0]);
});
StringBuilder sb = new StringBuilder();
for(int i=0;i<26&&m[i][0]==maxFreq;i++){
char c = (char) ('a' + m[i][2]);
sb.append(c);
}
return sb.toString();
}
}
7. 公共前缀:LC 3029 将单词恢复初始状态所需的最短时间Ⅰ
因为每次添加的字符数量和删去的一致,所以每次删完再添加后都有机会能恢复成原来的字符串。那么最早的时间点就是删剩下的字符串为原字符串的前缀的时刻。如果删完了都没找到前缀,那么此时一定已经可以恢复成原字符串。(因为后续补的字符是任意的)
class Solution {
public int minimumTimeToInitialState(String word, int k) {
StringBuilder sb = new StringBuilder(word);
int ans = 0;
while(!sb.isEmpty()){
if(sb.length()<k){
return ans+1;
}
ans++;
sb.delete(0,k);
if(isPrefix(sb.toString(),word)){
return ans;
}
}
return ans;
}
private boolean isPrefix(String rest,String target){
char[] ch1 = rest.toCharArray();
char[] ch2 = target.toCharArray();
for (int i = 0; i < ch1.length; i++) {
if(ch1[i]!=ch2[i]){
return false;
}
}
return true;
}
}
8. 坐标位置关系:LC 3027 人员站位的方案数Ⅱ
VP的123双周赛T4。这题O(n^3)过不了的。有个O(n^2)的做法。
首先要确定Alice的位置,即矩形(线段)左上角,那么就先对数组先按照x从小到大,再按照y从大到小排序,这样的话就只需要从这个点后面去枚举Bob的位置。
那么后续的节点那个能成为合法的呢?首先我们要保证后续的节点的y值≤Alice位置的y值。不然按题意,如果>该值就变成右上角了。
其次排序保证了x一定从小到大。所以在考虑限制Bob位置的因素时,x就可以不用考虑了。类似于:
在这张图里,如果把B1作为了Bob的位置,那么B2,B3由于排序,x值都将大于等于B1的x的值,那么就不会出现在区域中。(那么等于B1的x的值时会正好卡在边界上,怎么办?交给y来处理)
那么考虑后续点和Alice的位置形成的矩形区域是否会囊括进其他点的因素,就只有y值了。在上图中,试想以B3作为Bob的位置,那么B3将囊括B1和B2。即不合法。为什么呢?本质上是因为B3的y值并非是≤Alice位置的y值的最大值,这样在从左向右遍历中,B3就会包含那些y值高于其y值的点。
所以我们要做两件事情:
- 确保遍历到的点的y值≤Alice位置的y值
- 确保遍历到的点的y值是目前遍历到的所有合法y值中的最大值(但不用考虑后面的,因为x超出去了),这样也就解决了x值正好卡在边界上的问题(也即x卡边界就看y是否是当前合法中的最大值)
这个思路其实还挺巧妙的,实现起来也简单。
import java.util.Arrays;
class Solution {
public int numberOfPairs(int[][] points) {
Arrays.sort(points, (a, b) -> a[0] != b[0] ? a[0] - b[0] : b[1] - a[1]);
int ans = 0;
for (int i = 0; i < points.length; i++) {
int maxY = Integer.MIN_VALUE;
int y0 = points[i][1];
for (int j = i + 1; j < points.length; j++) {
int y = points[j][1];
if (y <= y0 && y > maxY) {
ans++;
maxY = y;
}
}
}
return ans;
}
}
9. 正难则反: LC 3002 移除后集合的最多元素数
这题首先要察觉的一个想法,那就是可以把这题转换为:
nums1中有n1个不同元素,nums2中有n2个不同元素,nums1和nums2总共有common个不同的公共元素,从n1个不同元素中删一半,从n2个不同元素中删一半,然后剩余的组合起来,不同的最多有多少。
为什么这么说呢?我们可以看这个例子:
nums1 = [1,1,1,1]
nums2 = [1,1,2,2]
按原题意
{1,1} {1,1} {2,2}
前面的{1,1}是nums1去掉公共元素后的剩余部分,后面的{2,2}是nums2去掉公共元素后的剩余部分。中间的{1,1}是公共元素。
这里体现正难则反了。我们如果不知道怎么删,可以去计算该怎么加。也就是从nums1中选n/2个元素,从nums2中取n/2个元素。然后并集后不同元素数最大。
在这个例子中,显然nums1选{1,1},nums2选{2,2},但是这样不同元素数量还是2,因为两个1和两个2其实是白选了,分别都只有一个生效。
而如果nums1的非公共部分中不满n/2个,我们还得从common中给它补齐,这个时候如果重复挑选common中的元素,也只会生效一次,所以common中也要是不同元素。也就等价于:
{1} {1} {2}
然后可以动手了:
- 先从n1个元素中选取不在common中的,但最多不超过n/2,所以c1 = min(n1-common,n/2)
- 再从n2个元素中选取不在common中的,但最多不超过n/2,所以c2 = min(n2-common,n/2)
- 这个时候已经挑了c1+c2个元素了,还得挑n-c1-c2个元素(如果还需要),这一部分只能从common中补齐(注意不能从nums1和nums2中的非公共部分中挑,因为1和2已经挑过了,再挑就是重复的,不算,还不如从公共的且非重复的common里面去挑),但最多只能挑common个,因此c3 = min(n-c1-c2,common)
答案就是c1+c2+c3。
import java.util.Arrays;
import java.util.HashSet;
class Solution {
public int maximumSetSize(int[] nums1, int[] nums2) {
HashSet<Integer> s1 = new HashSet<>();
HashSet<Integer> s2 = new HashSet<>();
for (int i : nums1) {
s1.add(i);
}
int common = 0;
for (int i : nums2) {
if(s2.contains(i)){
continue;
}
s2.add(i);
if(s1.contains(i)){
common++;
}
}
int n = nums1.length;
int c1 = Math.min(s1.size()-common,n/2);
int c2 = Math.min(s2.size()-common,n/2);
return c1+c2+Math.min(n-c1-c2,common);
}
}
注意在求公共部分时,要保证common中的元素也互不相同。因此当s2中已经包括某个数时,他之前已经经过了s1.contains的审判,注意不要重判了。
10. 货仓选址:LC 3086 拾起K个1需要的最少行动次数
<https://www.bilibili.com/video/BV1RH4y1W7DP/?spm_id_from=333.1365.list.card_archive.click&vd_source=b408ab4c35f1aa86e5d9431d34e3aeac>
这道题可以这么考虑:
- 首先在不增加任何新的1的情况下,应该从哪里开始作为立足点?很显然,如果一个位置自己就是1,且相邻两个位置也都是1,就可以通过2次操作就收集到3个1(本身0次,相邻2次),但如果相邻位置不是1,那就只能退而求其次了。所以我们求得是连续的1的个数的最大长度(仅考虑1,2,3,再远不相邻的话,就要多次操作2收集了)
- 然后是如果我们能增加新的1,那么怎么办?很简单,应该在相邻位置上设置1,然后用操作2把它换到index处。这样就是两次操作收集到1个新的1(操作一+操作二)。如果maxChanges比较大,大于等于了k-c(c是1中提到的那个长度),我们就不用考虑仅用操作二拾取1的情况了,因为操作一+操作二一定不劣于仅用操作二拾取不相邻的1的。
- 最后当我们用完了所有的操作一,只能用操作二了。应该把位置选在哪里呢?我们考虑一个规模为拾取k-maxChanges个1的问题(因为maxChanges个可用操作一+操作二完成掉)。那么我们查看原数组中的拥有k-maxChanges个1的子数组,然后选取第(k-maxChanges)/2个1作为index。这个就是货舱选址问题,中位数贪心。也即n个货舱,我们要选一个位置,让这个位置到n个货舱的距离和最小。这个位置就是中间那个货舱的位置(如果货舱个数为偶数个,那么中间-1和中间+1的那两个货舱及其之间任意位置均可)。这个证明比较简单。
- 假设总共我们有p个1,我们可以把所有的1的位置记录在一个pos数组中。随后从中选出连续的k-maxChanges个位置进行货仓选址,维护最小值就行了。这个问题可以用一个前缀和来解决。假设中位数的索引是height,那么第一个数到中位数产生的距离和就是这一段的数的个数乘以中位数再减去这一段距离的前缀和;中位数到最后一个数产生的距离和就是这一段的前缀和减去这一段的数的个数乘以中位数。
- 最后我们给答案加上maxChanges*2即可。
- 注意开long防爆int
import java.util.ArrayList;
class Solution {
public long minimumMoves(int[] nums, int k, int maxChanges) {
ArrayList<Integer> pos = new ArrayList<>();
int c = 0;
for (int i = 0; i < nums.length; i++) {
if(nums[i]==1){
pos.add(i);
if(i>0&&nums[i-1]==1){
if(i>1&&nums[i-2]==1){
c = 3;
}else{
c = Math.max(c,2);
}
}else{
c = Math.max(c,1);
}
}
}
c = Math.min(c,k);
if(maxChanges>=k-c){
// c-1个可以用操作2得到,1个不用动,k-c个可以操作1+操作2得到
return Math.max(c-1,0)+(k-c)*2L;
}
long[] pre = new long[pos.size()+1];
for (int i = 0; i < pos.size(); i++) {
pre[i+1] = pre[i]+pos.get(i);
}
long ans = Long.MAX_VALUE;
int size = k-maxChanges;
for(int i=size;i<=pos.size();i++){
// [left,mid]∪[mid,i]
int left = i-size;
int mid = left+size/2;
long height = pos.get(mid);
long lt = height *(mid-left)-(pre[mid]-pre[left]);
long gt = (pre[i]-pre[mid])- height *(i-mid);
ans = Math.min(ans,lt+gt);
}
return ans+maxChanges*2L;
}
}