# time O(N) space O(1)
"""
main idea: choose 1st element means last one can not be choosen. not chooen 1st element means last element can be an option to choose( not determined)=> valid loop range will be correspondinglly [0,len(nums)-1] and [1,len(nums)-2].since loop range differs, we need to discuss in two loops.
Hence we create a general loop body for getting maximum from current choices within valid ranges.

step1: edge case: len(nums) = 0 |len(nums) = 1
step2: two cases => 1. choose 1st element 2. not choose 1st elemenet
    2.1: given start, end point, loop through nums, record previous rob actions and based on previous actions to get current most optimal gains
    2.2 update previous actions by assigning current optimal to previous record since next loop current action will be recaculated.
step3: compare max gains from two cases above, get final max
"""
# time O(N) space O(1)
class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:return 0
        if len(nums) == 1: return nums[0]
        exclude_1 = self.helper(nums,1,len(nums)-1)
        include_1 = self.helper(nums,0,len(nums)-2)
        return max(exclude_1,include_1)
    def helper(self,arr,s,e):
        #step1: create middle cache for pre actions' best result so far 
        pre_rob, pre_not_rob = 0,0
        for i in range(s,e+1):
            # step2 based on previous action to get current best results ,given rob or not_rob cases 
            rob, not_rob = pre_not_rob+arr[i] , max(pre_rob,pre_not_rob)
            # step3: update previous best actions, move forward 
            pre_rob,pre_not_rob = rob,not_rob
        return max(rob,not_rob)

DP solutiton not optimal

class Solution(object):
    def rob(self, nums):
    # time O(N) space O(N)
        res = 0
          # case1: rop 1st one
        size = len(nums)
        if not nums:
            return 0
        if  size ==1:
            return  nums[0]
        memo1 = [0] * size
        memo2 = [0] * size
        #case: use 1st one  
        for i in range(0,size-1):
            memo1[i] = max(nums[(i-1+size)%size] + memo1[(i-2+size)%size],memo1[(i-1+size)%size])
        # case: skip 1st one 
        for i in range(1,size):
            memo2[i] = max(nums[(i-1+size)%size] + memo2[(i-2+size)%size],memo2[(i-1+size)%size])
        return max(max(memo1),max(memo2))

little optimize

class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        res = 0
          # case1: rop 1st one
        size = len(nums)
        if not nums:
            return 0
        if  size ==1:
            return  nums[0]
        memo1 = [0] * (size+1)
        memo2 = [0] * (size+1)
        # preprocess
        memo1[0] = 0
        memo1[1] = nums[0]
        for i in range(2,size+1):
            # case1: not skip 1st one 
            memo1[i] = max(nums[i-1] + memo1[(i-2)],memo1[i-1])
            # case2: skip 1st one 
            memo2[i] = max(nums[i-1] + memo2[(i-2)],memo2[i-1])
        print(memo1,memo2)
               # cannot get last one    can get last on 
        return max(memo1[size-1],memo2[size])